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:
{
  "development": {
    "apiTokenEnforcer": false
  },
  "integration": {
    "apiTokenEnforcer": false
  },
  "production": {
    "apiTokenEnforcer": true
  }
}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:
{
  "development": {
    "devOnlyFeature": true,
    "prodOnlyFeature": false
  },
  "production": {
    "devOnlyFeature": false,
    "prodOnlyFeature": true
  }
}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).