Rich Browne
Rich Browne

Reputation: 323

Re-importing modules between mocha tests

In my node/typescript express app I'm storing config settings in a settings.json file which is loaded and exported as an object by config.ts. Each module that uses the config settings imports the module like so:

import Config from './config';

config.ts looks like this (simplified for this example):

class Config {
  public static get(): any {
    const settings = require('settings.json');
    return settings;
  }
}

export default Config.get();

This all works fine when the app is running. However I'm having issues with my mocha tests. In some of the tests I want to change the config settings before triggering app functions (e.g. Config.someSetting = 'someValue'), then reset the config settings back to default before running the next test.

I know I can manually reset each changed config value back to the default value, but ideally I would like to "re-import" the config.ts module which will reset all config settings to their defaults. My question is what is the best way to do this?

I have tried using decache and adding the following to afterEach:

decache('./config');

and even though I can see that config.ts is no longer in the require cache, the Config object still exists with it's current values for all subsequent tests (config.ts is not being "re-imported").

What am I doing wrong?

Upvotes: 2

Views: 4115

Answers (2)

Shalbert
Shalbert

Reputation: 1154

The best approach I've found is to use proxyquire.

const proxyquire = require('proxyquire');

let moduleUnderTest;

describe('Given a Service Provider', () => {
    beforeEach(() => {
        proxyquire.noPreserveCache(); // Tells proxyquire to not fetch the module from cache

        // Ensures that each test has a freshly loaded instance of moduleUnderTest
        moduleUnderTest = proxyquire(
            '../../../../src/data/firebase/admin/service-provider',
            {} // Stub if you need to, or keep the object empty
        );
    });

    // Use moduleUnderTest as you like
});

Upvotes: 2

Estus Flask
Estus Flask

Reputation: 223114

Cache-mangling packages like decache should work in this case if require('settings.json') is reevaluated after decache('settings.json'), i.e. Config.get() is called.

Since it is settings.json module object that is modified, it should be restored. decache should directly affect the package that should be de-cached, i.e. settings.json. If Config.get() isn't called more than once, ./config' and every module that imports it should be de-cached as well. This makes the use of decache unreasonable in this case.

The problem here is that configuration module isn't test-friendly. Static-only classes are antipattern. If Config isn't exported as the code shows, this is antipattern as well, because it provides an abstraction cannot be used more than once, on module export.

In order to improve the situation, configuration module should be refactored in a way it will be able to reevaluate require('settings.json') in modules that use configuration object after it was imported:

export default function getConfig() {
  return require('settings.json');
}

getConfig() should be always used as is, it shouldn't be assigned const config = getConfig() at the top of a module where it is used, this will make it uncacheable.

Currently a way to restore original config is to modify it while keeping a reference to existing object, e.g.:

afterEach(() => {
  decache('./settings.json');
  Object.assign(Config, require('./settings.json'));
});

As it can be seen. Config.get abstraction doesn't help anything.

Another way in transpiled ES module is to patch module object directly. Since module object is supposed to be read-only reflection of exports according to the specs. It's expected that modules are treated accordingly by transpilers, including TypeScript. This depends on how the application is being built and may not work as expected in any environment.

import Config from './config';
console.log(Config.foo);

should be transpiled to something like

Object.defineProperty(exports, "__esModule", { value: true });    
console.log(config_1.default.foo;);

This may allow to mangle ES module exports dynamically (not possible for CommonJS module default exports) and affect those module parts that use Config and re-evaluated (e.g. inside functions but not in top-level module scope):

afterEach(() => {
  decache('./settings.json');
  const configModule = require('./config'));
  configModule.default = require('./settings.json');
});

Upvotes: 2

Related Questions