Search code examples
node.jsexpressunit-testingmocha.jssinon

Stub environment variables using Sinon in Mocha unit tests for Express routes


I'm trying to make it so when calling test routes in my TypeScript API any code that requires environment variables is just abstracted away so we aren't connecting to any real servers etc.

My initial express code is

router.post("/hello", async (
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
) => {
  const value = await getAValue(req.body)
  ...
}

The getAValue() function is what uses an environment variable, so I'm writing a test for this route using Supertest like so

  import request from "supertest";
  import App, { getAValue } from "./src";

  describe("POST /hello", () => {
    it("Returns 200 on expected input", (done) => {
      const payload = {
        "foo":"bar"
      }

      request(App)
        .post("/api/hello")
        .send(payload)
        .expect(200)
        .end((err) => {
          if (err) {
            done(err)
          } else {
            done();
          }
        });
     });
  });

I'm stubbing any required functions using stubs file test/stubs.ts

import sinon from "sinon"
import { getAValue } from "../src"

sinon.stub(getAValue);

and using my test script in my package.json to find these stubs as

"test": "mocha -r ts-node/register -r test/stubs.ts --config=test/.mocharc.json 'test/**/*.ts' --exit"

So within the getAValue() function there is a

import * as env from "env-var";

const value = env.get("VAR_NAME").required().asString();

But I'm somehow unable to mock anything related to this and always get the same error ERROR: EnvVarError: env-var: "VAR_NAME" is a required variable, but it was not set

I've tried mocking that function, or specifically the call to process.env but neither work.

What is the right way to mock env vars for Express route testing?


Solution

  • In your situation, I have 2 alternatives: a) stub getAValue() and b) stub env.get(). You can choose it depend on your condition.

    I use your code and modify it a little for this complete example:

    // 1. File: src/index.ts
    import express from 'express';
    import * as util from './util';
    
    const app = express();
    
    app.use(express.json());
    
    app.post('/hello', async (
      req: express.Request,
      res: express.Response,
      next: express.NextFunction
    ) => {
      // Need to know whether this function get called.
      console.log('/hello called');
      // Note: Do not use destructure object to call getAValue().
      const value = util.getAValue(req.body);
      // Check for return value.
      console.log('getAValue return:', value);
      // Simplify the request end.
      res.end();
    });
    
    export default app;
    
    // 2. File: src/util.ts
    import env from 'env-var';
    
    const getAValue = (body: any) => {
      // Need to know whether this function get called.
      console.log('Real getAValue called');
      // This is based on your code.
      const value = env.get('VAR_NAME').required().asString();
      return value;
    };
    
    export { getAValue };
    
    // 3. File: test/index.spec.ts
    import request from 'supertest';
    import app from '../src';
    
    describe('POST /hello', () => {
      it('Returns 200 on expected input', (done) => {
        const payload = {
          'foo':'bar'
        }
    
        request(app)
          .post('/hello')
          .send(payload)
          .expect(200)
          .end((err) => {
            if (err) {
              done(err)
            } else {
              done();
            }
          });
       });
    });
    

    Now the important file: test/stubs.ts

    Alternative a) stub getAValue()

    // 4a File: test/stubs.ts
    import sinon from 'sinon';
    import * as util from '../src/util';
    
    sinon.stub(util, 'getAValue').callsFake((input: any) => {
      console.log('fake getAValue called');
      // Input here is: req.body or payload from test.
      return input;
    });
    

    The result when I run it from terminal:

    $ npm run test
    
    > 69006601@1.0.0 test
    > mocha -r ts-node/register -r test/stubs.ts 'test/**/*.ts' --exit
    
    
    
      POST /hello
    /hello called
    fake getAValue called
    getAValue return: { foo: 'bar' }
        ✔ Returns 200 on expected input
    
    
      1 passing (22ms)
    

    In this alternative, the real getAValue function never get called and fake getAValue function get called. You can set test env inside fake function.

    Alternative b) stub env.get()

    // 4b File: test/stubs.ts
    import sinon from 'sinon';
    import env from 'env-var';
    
    sinon.stub(env, 'get').callsFake(function (input: any) {
      // Input is VAR_NAME.
      console.log('fake env.get called: ', input);
      // Supply with test env.
      return env.from({ VAR_NAME: 'test' }).get(input);
    });
    

    The result when I run it from terminal:

    $ npm run test
    
    > 69006601@1.0.0 test
    > mocha -r ts-node/register -r test/stubs.ts 'test/**/*.ts' --exit
    
    
    
      POST /hello
    /hello called
    Real getAValue called
    fake env.get called:  VAR_NAME
    getAValue return: test
        ✔ Returns 200 on expected input
    
    
      1 passing (22ms)
    

    In this alternative, the real getAValue get called, but the real env.get() not get called, so you can set env for test.