Search code examples
javascriptunit-testingmocha.jssinones6-class

Mocking a class property set in the constructor using Sinon


I'm trying to mock a class property that is set to a default value inside the constructor

class Files {
  constructor(queueNumber = 0) {
    this.queueNumber = queueNumber;
    this.dir = 'JiraResults';
    if (!fs.existsSync(this.dir)) {
      fs.mkdirSync(this.dir);
    }
  }
  ...
}

The constructor creates directories and files based on the dir property and for the tests, I want another directory, so I don't need to move the real directory to run the tests.

I tried many approaches to replace the property and all of them kept failing with different errors from Sinon.

First attempt:

const tempDir = 'JiraResults-TEMP';
let stubDir;

describe('Files', () => {
  before(() => {
    stubDir = sinon.stub(Files.prototype.constructor, 'dir').value(tempDir);
  }
  ...
}

With this I get the error TypeError: Cannot stub non-existent own property dir

Second attempt

const tempDir = 'JiraResults-TEMP';
let stubDir;

describe('Files', () => {
  before(() => {
    stubDir = sinon.stub(Files.prototype, 'dir').value(tempDir);
  }
  ...
}

With this I get the error TypeError: Cannot stub non-existent own property dir

Third attempt

const tempDir = 'JiraResults-TEMP';
let stubDir;

describe('Files', () => {
  before(() => {
    stubDir = sinon.stub(Files.prototype, 'this').value({
      dir: sinon.stub().returnsThis(tempDir),
    });
  }
  ...
}

With this I get the error TypeError: Cannot stub non-existent own property this


I also tried other things and never got to the point of having the property replaced.

I looked into Sinon documentation, but none of the examples seems to apply to a constructor class.

Could anyone give me a working example on how can I replace this property?

Thanks.


Solution

  • You can change the value of the dir property directly so that the method under test will use the stubbed dir.

    E.g.

    files.js:

    class Files {
      constructor(queueNumber = 0) {
        this.queueNumber = queueNumber;
        this.dir = "JiraResults";
      }
    
      mkdir() {
        console.log("make dir: ", this.dir);
      }
    }
    
    module.exports = Files;
    

    files.test.js:

    const Files = require("./files");
    const sinon = require("sinon");
    
    describe("Files", () => {
      it("should use stubbed dir", () => {
        sinon.spy(console, "log");
        const instance = new Files();
        instance.dir = "stubbed dir";
        instance.mkdir();
        sinon.assert.calledWith(console.log, "make dir: ", "stubbed dir");
      });
    });
    

    Unit test result:

    Files
    make dir:  stubbed dir
        ✓ should use stubbed dir
    
    
      1 passing (7ms)
    
    ---------------|----------|----------|----------|----------|-------------------|
    File           |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
    ---------------|----------|----------|----------|----------|-------------------|
    All files      |      100 |      100 |      100 |      100 |                   |
     files.js      |      100 |      100 |      100 |      100 |                   |
     files.test.js |      100 |      100 |      100 |      100 |                   |
    ---------------|----------|----------|----------|----------|-------------------|
    

    UPDATE:

    I don't think it's possible. Let's take a look at below example:

    files.js:

    const fs = require("fs");
    
    class Files {
      constructor(queueNumber = 0) {
        this.queueNumber = queueNumber;
        console.log("before: ", this.dir);
        this.dir = "JiraResults";
        console.log("after: ", this.dir);
        if (!fs.existsSync(this.dir)) {
          fs.mkdirSync(this.dir);
        }
      }
    }
    
    Files.prototype.dir = "";
    
    module.exports = Files;
    

    files.test.js:

    const Files = require("./files");
    const sinon = require("sinon");
    const fs = require("fs");
    
    describe("Files", () => {
      it("should use stubbed dir to mkdir", () => {
        sinon.stub(fs, "existsSync").returns(false);
        sinon.stub(fs, "mkdirSync");
        sinon.stub(Files.prototype, "dir").value("stubbed dir");
        console.log("stub dir");
        new Files();
        sinon.assert.calledWith(fs.existsSync, "stubbed dir");
        sinon.assert.calledWith(fs.mkdirSync, "stubbed dir");
      });
    });
    

    Unit test result:

      Files
    stub dir
    before:  stubbed dir
    after:  JiraResults
        1) should use stubbed dir to mkdir
    
    
      0 passing (13ms)
      1 failing
    
      1) Files
           should use stubbed dir to mkdir:
         AssertError: expected existsSync to be called with arguments 
    JiraResults stubbed dir 
          at Object.fail (node_modules/sinon/lib/sinon/assert.js:106:21)
          at failAssertion (node_modules/sinon/lib/sinon/assert.js:65:16)
          at Object.assert.(anonymous function) [as calledWith] (node_modules/sinon/lib/sinon/assert.js:91:13)
          at Context.it (src/stackoverflow/59303752/files.test.js:1:2345)
    

    We can make a stub for property dir successfully before instantiating the class Files. But after we instantiated the class Files. It will assign "JiraResults" string to property dir which means it will replace the stubbed dir to JiraResults.

    There are three options:

    1. Set a default value for property dir, Files.prototype.dir = 'JiraResults'(But it is based on your requirement.)
    2. Pass dir as a parameter to the constructor of Files class.
    3. Extract the fs operations to a method like the original anwser.

    Then, it will be easier to make a stub for unit testing.