Search code examples
javascriptnode.jsasynchronousmocha.jssinon

stubbing method with async callback in sinon


I have a parsePDF() method that calls extractText(), which returns its results in an async callback.

the question

How do I write a test that tests only that parsePDF calls extractText once, and with whatever path argument was passed to parsePDF? (I have separate unit tests for extractText and cleanUp.)

Here's the basic structure of the parsePDF method:

Parser.parsePDF(path, callback) {
  Parser.extractText(path, function gotResult(err, raw_text) {
    if (err) {
      callback(err)
      return;
    }
    var clean_text = Parser.cleanUp(raw_text)
    callback(null, clean_text);
  });
};

what I've tried

Despite reading the Sinon documentation on callsArg, Mocha/Chai/Sinon tutorials, various SO posts such as this one about stubbing function with callback - causing test method to timeout, I still haven't grokked what's needed to write a proper test.

This attempt fails with the message

Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

It makes sense, the callback isn't being fired

    it('should call extractText() with path argument', function(done) {
        sandbox.stub(Parser, 'extractText')
        Parser.parsePDF('a known path', function(err, bill) {
            expect(sinon).calledOnce(Parser.extractText).calledWith('a known path')
            done()
        });
    });

But the following with yeilds() also fails with the message undefined is not a function pointing at the expect... line:

    it('should call extractText() with path argument', function(done) {
        sandbox.stub(UtilityBillParser, 'extractText').yields(null, 'some text')
        Parser.parsePDF('a known path', function(err, bill) {
            expect(sinon).calledOnce(Parser.extractText).calledWith('a known path')
            done()
        });
    });

As does the following with .callsArg(1):

    it('should call extractText() with path argument', function(done) {
        sandbox.stub(UtilityBillParser, 'extractText').callsArg(1)
        UtilityBillParser.parsePDF('a known path', function(err, bill) {
            expect(sinon).calledOnce(UtilityBillParser.extractText).calledWith('a known path')
            done()
        });
    });

Solution

  • Here is the unit test solution:

    parser.js:

    const Parser = {
      parsePDF(path, callback) {
        Parser.extractText(path, function gotResult(err, raw_text) {
          if (err) {
            callback(err);
            return;
          }
          var clean_text = Parser.cleanUp(raw_text);
          callback(null, clean_text);
        });
      },
      extractText(path, callback) {
        callback();
      },
      cleanUp(rawText) {
        return "real clean text";
      },
    };
    
    module.exports = Parser;
    

    parser.test.js:

    const Parser = require("./parser");
    const sinon = require("sinon");
    
    describe("Parser", () => {
      afterEach(() => {
        sinon.restore();
      });
      describe("#parsePDF", () => {
        it("should clean up raw test", () => {
          const callback = sinon.stub();
          sinon.stub(Parser, "extractText").yields(null, "fake raw text");
          sinon.stub(Parser, "cleanUp").returns("fake clean text");
          Parser.parsePDF("./somepath", callback);
          sinon.assert.calledWith(Parser.extractText, "./somepath", sinon.match.func);
          sinon.assert.calledWith(Parser.cleanUp, "fake raw text");
          sinon.assert.calledWith(callback, null, "fake clean text");
        });
    
        it("should handle err", () => {
          const callback = sinon.stub();
          const mError = new Error("some error");
          sinon.stub(Parser, "extractText").yields(mError, null);
          sinon.stub(Parser, "cleanUp").returns("fake clean text");
          Parser.parsePDF("./somepath", callback);
          sinon.assert.calledWith(Parser.extractText, "./somepath", sinon.match.func);
          sinon.assert.calledWith(callback, mError);
        });
      });
    });
    

    Unit test result with coverage report:

      Parser
        #parsePDF
          ✓ should clean up raw test
          ✓ should handle err
    
    
      2 passing (9ms)
    
    ----------------|----------|----------|----------|----------|-------------------|
    File            |  % Stmts | % Branch |  % Funcs |  % Lines | Uncovered Line #s |
    ----------------|----------|----------|----------|----------|-------------------|
    All files       |    93.75 |      100 |    77.78 |    93.75 |                   |
     parser.js      |       80 |      100 |       50 |       80 |             13,16 |
     parser.test.js |      100 |      100 |      100 |      100 |                   |
    ----------------|----------|----------|----------|----------|-------------------|
    

    Source code: https://github.com/mrdulin/mocha-chai-sinon-codelab/tree/master/src/stackoverflow/30163720