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).