Search code examples
javascriptunit-testingexpressmocha.jssinon

Sinon stub seems to not be called at all in test even though it works in previous test


I am trying to test just the function of an express router with sinon. My tests below, the first test passes just fine no issues. The second however doesn't pass. I can't figure out why.

If I send an http request to the route it works as expected.

Something about the catch is causing issues. Below is code I was able to whittle it down to and the error

books.js

import express from 'express';
import models from '../db/models';
const router = express.Router();

var indexPost = async (req, res, next) => {
  try {
    let savedBook = await models.Book.create({
      title: req.body.title || null,
      isbn: req.body.isbn || null,
      author: req.body.author || null
    });
    res.status(201).json({ book: savedBook.id });
  } catch (err) {
    res.status(400).send('');
  }
};
router.post('/', indexPost);

export default router;
export { indexPost };

books.test.js

import { indexPost } from '../../../src/routes/books';
import models from '../../../src/db/models';
import sinon from 'sinon';
import { expect } from 'chai';
import sinonTestFactory from 'sinon-test';

const sinonTest = sinonTestFactory(sinon);

describe('Books router', () => {
  describe('indexPost', () => {
    it('should save the book to the database', sinonTest(async function () {
      let req = {
        body: {
          title: 'Book Title',
          isbn: '123asera23',
          author: 123
        }
      };

      let res = {
        status: status => {},
        json: json => {}
      };

      this.stub(res, 'status').returns(res);
      this.stub(res, 'json').returns(res);

      indexPost(req, res);

      let book = await models.Key.findById(1);

      expect(book.title).to.equal('Book Title');
      expect(book.isbn).to.equal('123asera23');
      expect(book.author).to.equal(123);

      sinon.assert.calledWith(res.status, 201);
      sinon.assert.calledWith(res.json, { book: 1 });
    }));

    it('should throw an error if data is not all there', sinonTest(async function () {
      let req = {
        body: {
          title: 'Book Title',
          author: 123
        }
      };

      let res = {
        status: status => {},
        send: send => {}
      };

      this.stub(res, 'status').returns(res);
      this.stub(res, 'send').returns(res);

      indexPost(req, res);

      sinon.assert.calledWith(res.status, 400);
      sinon.assert.calledWith(res.send, '');
    }));
  });
});

Error

1) Books router
    indexPost
        should throw an error if data is not all there:
            AssertError: expected status to be called with arguments
            at Object.fail (/var/app/node_modules/sinon/lib/sinon/assert.js:96:21)
            at failAssertion (/var/app/node_modules/sinon/lib/sinon/assert.js:55:16)
            at Object.assert.(anonymous function) [as calledWith] (/var/app/node_modules/sinon/lib/sinon/assert.js:80:13)
            at Context.<anonymous> (tests/src/routes/books.test.js:58:20)
            at Generator.next (<anonymous>)
            at step (tests/src/routes/books.test.js:21:191)
            at tests/src/routes/keys.test.js:21:437
            at new Promise (<anonymous>)
            at Context.<anonymous> (tests/src/routes/books.test.js:21:99)
            at callSandboxedFn (/var/app/node_modules/sinon-test/lib/test.js:94:25)
            at Context.sinonSandboxedTest (/var/app/node_modules/sinon-test/lib/test.js:114:24)

Solution

  • Since no one looked at this to realize it wasn't as simple a unequal assertion I'll post the real answer I finally figured out.

    Basically I was approaching the tests wrong. I was only half accounting for non-async code inside of javascript.

    What I need to do was return a promise to mocha's it function. In order to do that my controller needed to return a promise as well. In the case of doing database stuff I could just return the database call promise.

    Once the promise calls either resolve or reject. You can then do your assertions to see if the tests work.

    They key is you have to chain your promises from the bottom of your controller all the way back up to the it function of mocha.

    Below is the code to resolve this.

    import express from 'express';
    import models from '../db/models';
    const router = express.Router();
    
    var indexPost = (req, res, next) => {
      return models.Book.create({
        title: req.body.title || null,
        isbn: req.body.isbn || null,
        author: req.body.author || null
      }).then(savedBook => {
        res.status(201).json({ book: savedBook.id });
      }).catch(err => {
        res.status(400).send('');
      });
    };
    router.post('/', indexPost);
    
    export default router;
    export { indexPost };
    
    import { indexPost } from '../../../src/routes/books';
    import models from '../../../src/db/models';
    import sinon from 'sinon';
    import { expect } from 'chai';
    
    describe('Books router', () => {
      describe('indexPost', () => {
        it('should save the book to the database', async () => {
          let req = {
            body: {
              title: 'Book Title',
              isbn: '123asera23',
              author: 123
            }
          };
    
          const jsonStub = sinon.stub()
          const res = { status: status => ({ json: jsonStub, send: err => err }) }
          const statusSpy = sinon.spy(res, 'status')
    
          return indexPost(req, res).then(() => {
            let book = await models.Key.findById(1);
    
            expect(book.title).to.equal('Book Title');
            expect(book.isbn).to.equal('123asera23');
            expect(book.author).to.equal(123);
    
            sinon.assert.calledWith(res.status, 201);
            sinon.assert.calledWith(res.json, { book: 1 });
          })
        }));
    
        it('should throw an error if data is not all there', () => {
          let req = {
            body: {
              title: 'Book Title',
              author: 123
            }
          };
    
          const sendStub = sinon.stub()
          const res = { status: status => ({ json: err => err, send: sendStub }) }
          const statusSpy = sinon.spy(res, 'status')
    
          indexPost(req, res).then(() => {
            sinon.assert.calledWith(res.status, 400);
            sinon.assert.calledWith(res.send, '');
          });
        }));
      });
    });
    

    I also had some weirdness in my response object the code above resolves that as well.