Home > Net >  Mocking environment and/or exported constants in Jest for Node apps
Mocking environment and/or exported constants in Jest for Node apps

Time:10-21

Setup

I have a module in an express app that I am trying to test that uses constants exported from a module. Many of these constants are derived from environment variables.

// src/constants.js
export default {
  ID_SOURCE: process.env.ID_SOURCE,
  ID_PROVIDER: process.env.ID_PROVIDER
};

/*
process.env {
  ID_SOURCE: 'foo',
  ID_PROVIDER: 'bar'
}
*/

Here is the pattern to source these constants in the module under test.

import constants from './constants';
import { errors } from './errors';

const { ID_SOURCE, ID_PROVIDER } = constants;

export const moduleUnderTest = async (req, res) => {
  if (!ID_SOURCE || !ID_PROVIDER) {
    if (!ID_SOURCE) logService('ID_SOURCE not provided');
    if (!ID_PROVIDER) logService('ID_PROVIDER not provided');
    throw { err: errors.unexpectedError };
  };
  // ... do stuff
  return res.status(200).json({ cool: 'beans' });
}

I want to test the exception scenario where the environment variables are not present. This has proved difficult under the current pattern in the module, because even if I mock the environment, the module is being evaluated -- and the ID_SOURCE and ID_PROVIDER constants in the module are initialized -- before the environment changes.

Importantly, I only want to change the constants for ONE TEST. Here's what I've tried.

The test

import constants from './constants';
import { moduleUnderTest } from './moduleUnderTest';

describe('Test module', () => {

  beforeEach(() => {
    jest.clearAllMocks();
  })

  test('test exception scenario', async () => {
    jest.doMock('./constants', () => {
      const actualConstants = jest.requireActual('./constants');
      return {
        __esModule: true,
        ...actualConstants,
        ID_SOURCE: undefined,
        ID_PROVIDER: undefined
      }
    });

    // ... assume req/res are available
    try {
      await moduleUndertest(req, res);
    } catch(ex) {
      // by the time we get here, the variables are set from the environment still
      // because the module was evaluated and ID_SOURCE and ID_PROVIDER variables in the closure set
      // before mocking the constants
      expect(ex.message).toBeDefined();
    }
});

Mocking the environment will not work either for the reason stated in the comments above... the module is evaluated at import and those variables are initialized from the environment before the mock changes them.

Behavior

ID_SOURCE and ID_PROVIDER always have the values assigned to them from the environment.

Of course one obvious solution is just to access the variables out of the constants in the function definition, which will only execute at their invocation... but the problem is that in the codebase I am working in this pattern is EVERYWHERE. I can't just start arbitrarily refactoring everything to make this work, and I feel that there has to be a better way.

Other things I've tried

I've also tried mocked-env, which fails for the same reasons as stated above. I've tried calling jest.resetModules in a beforeEach hook and requiring in the module inside each test, but this pattern hasn't produced any results so far. I would be interested to hear more about this pattern if this is the way.

I thought that an approach with jest setupFiles might be the solution, as I use them in other cases where an environment variable needs to be available locally (everywhere) that isn't there, but I don't want these mocked globally for every test... only one test.

What is the right pattern for doing this?

CodePudding user response:

In my Jest configuration, I add a file that has my defined environment for the tests.

setupFiles: [
    '<rootDir>/src/.env.testing.js',
],

In this file, I then set the environment I use:

process.env.PORT='9999';

I then have my normal services read the configuration from the environment as needed. You can use any file name for the setupFiles, I was replicating my use of the .env file.

CodePudding user response:

Ultimately, this is what I did to fix it.

    test('should process exception', async () => {
        jest.resetModules(); // clear require.cache
        jest.doMock('../src/constants', () => {
            const constants = jest.requireActual('../src/constants');
            console.log(constants);
            return {
                __esModule: true,
                default: {
                    ...constants,
                    ID_SOURCE: 'foo'
                }
            }
        });

        const req = makeReq({ session: { email: "[email protected]" }});
        const res = makeRes();

        const { updateUserProfile } = await import(
            "../src/controller"
        );

        await updateUserProfile(req, res);

        expect(res.status).toHaveBeenCalledWith(500);
    })

One of the big issues was making sure that the require cache was clear before mocking the constants file. Then, it was simply a matter of dynamically importing the module under test after I had mocked the constants module.

  • Related