An export/import pattern for ES6 classes that allows for easy testing and stubbing of (injected) dependencies with a neat use of curly-braces when importing into a test file.
featureSwitches.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export class FeatureSwitches {
constructor(switchesFile = "./feature-switches.json") {
this.switches = JSON.parse(fs.readFileSync(switchesFile));
}
isOn(switchName) {
const env = process.env.NODE_ENV;
if (this.switches.hasOwnProperty(env) && this.switches[env].hasOwnProperty(switchName)) {
return this.switches[env][switchName];
} else {
return true;
}
}
isOff(switchName) {
return !this.isOn(switchName);
}
}
export default new FeatureSwitches();
Line 21 allows the opportunity to import
a new instance of FeatureSwitches, like so:
app.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
import FeatureSwitches from "../helpers/featureSwitches";
...
function createApp() {
const app = express();
...
if (FeatureSwitches.isOn("apiTokenEnforcer")) {
app.use(apiTokenEnforcer());
}
...
return app;
}
export default createApp;
apiTokenEnforcer
is essentially a piece of middleware.
NB on line 2 the instance of featureSwitches
is assigned to FeatureSwitches
. However, you could call it whatever you like eg import AnyName from "../helpers/featureSwitches";
console.log(process.cwd)
is useful for ascertaining what relative paths you need.
feature-switches.json:
This allowed us to include or omit (‘turn on’ or ‘turn off’) sections of code at will to control what is run in different environments.
const
is immutable and block-scoped
let
is mutable and block-scoped
var
is mutable and function-scoped
Everything should generally be assigned to const
in ES6 unless you’re planning to mutate the variable.
Tests
test/helpers/featureSwitches.js:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import { expect } from "chai";
import { FeatureSwitches } from "../../helpers/feature_switches";
...
let environment;
const changeEnv = function changeEnv(newEnv) {
environment = process.env.NODE_ENV;
process.env.NODE_ENV = newEnv;
};
const restoreEnv = function restoreEnv() {
process.env.NODE_ENV = environment;
};
describe("Feature Switches", () => {
describe('when in development environment', () => {
before(() => { changeEnv("development"); });
after(restoreEnv);
const featureSwitches = new FeatureSwitches('./fixtures/feature-switches/testFixture');
it('a development-only feature is switched on', () => {
expect(featureSwitches.isOn("devOnlyFeature")).to.be.true;
expect(featureSwitches.isOff("devOnlyFeature")).to.be.false;
});
it('a production-only feature is switched off', () => {
expect(featureSwitches.isOn("prodOnlyFeature")).to.be.false;
expect(featureSwitches.isOff("prodOnlyFeature")).to.be.true;
});
...
});
describe('when in production environment', () => {
before(() => { changeEnv("production"); });
after(restoreEnv);
const featureSwitches = new FeatureSwitches('./fixtures/feature-switches/testFixture');
it('a production-only feature is switched on', () => {
expect(featureSwitches.isOn("prodOnlyFeature")).to.be.true;
expect(featureSwitches.isOff("prodOnlyFeature")).to.be.false;
});
it('a development-only feature is switched off', () => {
expect(featureSwitches.isOn("devOnlyFeature")).to.be.false;
expect(featureSwitches.isOff("devOnlyFeature")).to.be.true;
});
...
});
});
testFixture.js:
Key Points
Line 2, by using curly braces {}
, imports the class name itself so you then have to create a new instance of it (as on Line 6).
Without the curly braces (as on line 2 of app.js), a new instance is created with the default json already having been passed in. This would make it impossible to pass in a different json file for the purpose of our test (line 20). Using this {}
technique solved the problem of satisfying both runtime and testing requirements nicely.
It was also very difficult to stub the feature-switches.js without injecting it as a dependency (featureSwitches.js
, line 2) as opposed to having it sat inside the class as a dependency - a good example of how dependency injection makes your code easier to work with (amongst other benefits).