'Jest mocks only work at top of file, never inside "describe" or "it" blocks

I'm trying to introduce a small change to an existing project without unit tests and decided I'd try to learn enough about nodejs and jest to include tests with my change. However, I cannot get mocks to work like I'd expect in, say, python. The project uses the "kubernetes-client" library from godaddy and tries to create a config object from the envvar "KUBECONFIG", like this:

// a few lines into server.js
// Instantiate Kubernetes client
const Client = require('kubernetes-client').Client
const config = require('kubernetes-client').config;
if (process.env.KUBECONFIG) {
    client = new Client({
        config: config.fromKubeconfig(config.loadKubeconfig(process.env.KUBECONFIG)),
        version: '1.13'
    });
}
else {
    client = new Client({ config: config.getInCluster(), version: '1.9' });
}

In my testing environment, I don't want any API calls, so I'm trying to mock it out:

// __tests__/server.test.js
// Set up mocks at the top because of how server.js sets up k8s client
const k8sClient = require('kubernetes-client');
const narp = 'narp';
jest.mock('kubernetes-client', () => {
    const noConfigmaps = jest.fn(() => {
        throw narp;
    });
    const namespaces = jest.fn().mockReturnValue({
        configmaps: jest.fn().mockReturnValue({
            get: noConfigmaps
        })
    });
    const addCustomResourceDefinition = jest.fn().mockReturnThis()
    const mockClient = {
        api: {
            v1: {
                namespaces
            }
        },
        addCustomResourceDefinition: jest.fn().mockReturnThis(),
    };
    return {
        Client: jest.fn(() => mockClient),
        config: {
            fromKubeconfig: jest.fn().mockReturnThis(),
            loadKubeconfig: jest.fn().mockReturnThis(),
            getInCluster: jest.fn().mockReturnThis()
        },
    };
});
const app = require('../server.js')
const supertest = require('supertest');
const requestWithSuperTest = supertest(app.app);
describe('Testing server.js', () => {
    afterAll(() => {
        app.server.close();
    });
    describe('Tests with k8s client throwing error when fetching configmaps', () => {
        it("finds a resource's ingressGroup by name", () => {
            var resource = {
                "spec": {
                    "ingressClass": "foo",
                    "ingressTargetDNSName": "foo"
                }
            };
            var ingressGroups = [
                {
                    "ingressClass": "bar",
                    "hostName": "bar",
                    "name": "barName"
                },
                {
                    "ingressClass": "foo",
                    "hostName": "foo",
                    "name": "fooName"
                }
            ];
            expect(app.findMatchingIngressGroupForResource(resource, ingressGroups)).toBe("fooName");
        });

        it('GET /healthcheck should respond "Healthy"', async () => {
            const resp = await requestWithSuperTest.get('/healthcheck');
            console.log("Response in Testing Endpoints: " + JSON.stringify(resp));
            expect(resp.status).toEqual(200);
            expect(resp.type).toEqual(expect.stringContaining('text'));
            expect(resp.text).toEqual('Healthy');
        });
        it('Tests getIngressGroups() rejects with error when it cannot get configmaps', async () => {
            app.getIngressGroups()
                .then()
                .catch(error => {
                    expect(error).toEqual("Failed to fetch Ingress Groups: " + narp);
                });
        });
    });
});

With this setup, the tests pass (although I suspect it's meaningless). If I try to move the mocks inside the describe or it block using a beforeEach function (or not) so that I can change the behavior to return mock data instead of throwing an error, I immediately get errors with the k8s client complaining it can't find my kubeconfig/clusterconfig:

$ npm run testj

> testj
> jest --detectOpenHandles

kubernetes-client deprecated require('kubernetes-client').config, use require('kubernetes-client/backends/request').config. server.js:45:44
kubernetes-client deprecated loadKubeconfig see https://github.com/godaddy/kubernetes-client/blob/master/merging-with-kubernetes.md#request-kubeconfig- server.js:49:42
 FAIL  __tests__/server.test.js
  ● Test suite failed to run

    ENOENT: no such file or directory, open 'NOT_A_FILE'

      44 | if (process.env.KUBECONFIG) {
      45 |     client = new Client({
    > 46 |         config: config.fromKubeconfig(config.loadKubeconfig(process.env.KUBECONFIG)),
         |                                              ^
      47 |         version: '1.13'
      48 |     });
      49 | }

      at node_modules/kubernetes-client/backends/request/config.js:335:37
          at Array.map (<anonymous>)
      at Object.loadKubeconfig (node_modules/kubernetes-client/backends/request/config.js:334:28)
      at Object.eval [as loadKubeconfig] (eval at wrapfunction (node_modules/kubernetes-client/node_modules/depd/index.js:425:22), <anonymous>:5:11)
      at Object.<anonymous> (server.js:46:46)

If anybody has run into this kind of behavior before or sees some obviously-wrong lines, I'd really appreciate any tips or information. Thanks!



Solution 1:[1]

I had to change a few things to get this working:

  • jest.doMock() instead of jest.mock()
  • use of let app inside the describe block instead of const app at module-scope
  • a beforeEach() which calls jest.resetModules()
  • an afterEach() which calls app.close()
  • in the it block which overrides the mock(s), explicitly call jest.resetModules() before overriding
  • in the it block which overrides the mock(s), call app.close() and re-initialize app before invoking the actual function-under-test/expect

Resulting test file:

// Set up mocks at the top because of how server.js sets up k8s client
const k8sClient = require('kubernetes-client');
const supertest = require('supertest');
const narp = 'narp';

describe('Testing server.js', () => {
    let app;
    let requestWithSuperTest;
    beforeEach(() => {
        jest.resetModules();
        jest.doMock('kubernetes-client', () => {
            const noConfigmaps = jest.fn(() => {
                throw narp;
            });
            const namespaces = jest.fn().mockReturnValue({
                configmaps: jest.fn().mockReturnValue({
                    get: noConfigmaps
                })
            });
            const addCustomResourceDefinition = jest.fn().mockReturnThis()
            const mockClient = {
                api: {
                    v1: {
                        namespaces
                    }
                },
                addCustomResourceDefinition: jest.fn().mockReturnThis(),
            };
            return {
                Client: jest.fn(() => mockClient),
                config: {
                    fromKubeconfig: jest.fn().mockReturnThis(),
                    loadKubeconfig: jest.fn().mockReturnThis(),
                    getInCluster: jest.fn().mockReturnThis()
                },
            };
        });
        app = require('../server.js');
        requestWithSuperTest = supertest(app.app);
    });
    afterEach(() => {
        app.server.close();
    });

    it("finds a Resource's ingressGroup by name", () => {
        var resource = {
            "spec": {
                "ingressClass": "foo",
                "ingressTargetDNSName": "foo"
            }
        };
        var ingressGroups = [
            {
                "ingressClass": "bar",
                "hostName": "bar",
                "name": "barName"
            },
            {
                "ingressClass": "foo",
                "hostName": "foo",
                "name": "fooName"
            }
        ];
        expect(app.findMatchingIngressGroupForResource(resource, ingressGroups)).toBe("fooName");
    });

    it('GET /healthcheck should respond "Healthy"', async () => {
        const resp = await requestWithSuperTest.get('/healthcheck');
        console.log("Response in Testing Endpoints: " + JSON.stringify(resp));
        expect(resp.status).toEqual(200);
        expect(resp.type).toEqual(expect.stringContaining('text'));
        expect(resp.text).toEqual('Healthy');
    });
    it('Tests getIngressGroups() rejects with error when it cannot get configmaps', async () => {
        expect.assertions(1);
        await app.getIngressGroups()
            .catch(error => {
                expect(error).toEqual("Failed to fetch Ingress Groups: " + narp);
            });
    });
    it('Tests getIngressGroups() succeeds when it gets configmaps', async () => {
        expect.assertions(1);
        jest.resetModules();
        jest.doMock('kubernetes-client', () => {
            const noConfigmaps = jest.fn(() => {
                console.log('Attempted to get mocked configmaps');
                return Promise.resolve({
                    body: {
                        items: []
                    }
                });
            });
            const namespaces = jest.fn().mockReturnValue({
                configmaps: jest.fn().mockReturnValue({
                    get: noConfigmaps
                })
            });
            const addCustomResourceDefinition = jest.fn().mockReturnThis()
            const mockClient = {
                api: {
                    v1: {
                        namespaces
                    }
                },
                addCustomResourceDefinition: jest.fn().mockReturnThis(),
            };
            return {
                Client: jest.fn(() => mockClient),
                config: {
                    fromKubeconfig: jest.fn().mockReturnThis(),
                    loadKubeconfig: jest.fn().mockReturnThis(),
                    getInCluster: jest.fn().mockReturnThis()
                },
            };
        });
        app.server.close();
        app = require('../server.js');
        await app.getIngressGroups()
            .then(result => {
                expect(result).toEqual([])
            });
    });
});

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Resisty