Reputation: 323
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
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
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