Search code examples
node.jsmockingmocha.jsprototypesinon

Spying on a prototype method in Sinon.js


With sinon, I'm trying to spy on a prototype method. It looks so simple in online howtos etc. I tried many websites and SO posts like this: Stubbing a class method with Sinon.js or sinon - spy on toString method, but it just doesn't work for me.

Prerequisites

I'm using http.d.ts https://github.com/nodejs/node/blob/master/lib/_http_outgoing.js to write data back from an async API call through an OutgoingMessage object:

class OutgoingMessage extends stream.Writable

There is a prototype method end in OutgoingMessage:

OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {

My API function is called like this:

Get = async (req:IncomingMessage,res:OutgoingMessage):Promise<void> => {...
    ...
    res.end(data)
    ...
}

My tests

In my test I'm calling the Get method. My IncomingMessage determines what I expect to be in the OutgoingMessage.

it("should call end with the correct value", async function(){ 
    ...
    let outgoingMessageSpy = sinon.spy(OutgoingMessage.prototype,"end");
    let anOutgoingMessage = <OutgoingMessage>{};
    ...
    expect(outgoingMessageSpy.calledOnce).to.be.true();
}

Debugging the test case I see how end is being called but apparently I have not set up my spy the right way as my expectation fails, calledOnce is false. Inspecting the object I see that calledCount is 0.

I'm doing basically the same (it appears to me) when I do

const toStringSpy = sinon.spy(Number.prototype, "toString");
expect(toStringSpy.calledWith(2)).to.be.true;

and that works. I do notice, though, that VS Code highlights the keyword prototype differently for Number.prototype and OutgoingMessage.prototype. Is that of relevance? On mouse-over is says NumberConstructor.prototype but only OutgoingMessage.prototype..

Questions

How to set up the spy correctly to pick up the call to the prototype method end?


Solution

  • You have set up the spy correctly. BUT, there are conditions which can make the spy fail. For example: I saw you use async function, maybe you are not awaiting correctly on your async function test. Example below to show you that condition.

    I have a very simple http server which has response with delay.

    I create your Get method like this:

    // File: get.ts
    import { IncomingMessage, OutgoingMessage } from 'http';
    import delay from 'delay';
    
    const Get = async (req: IncomingMessage, res: OutgoingMessage): Promise<void> => {
      console.log('Get start');
      // I add this to show you that this async function need to be awaited.
      await delay(200);
      res.end(JSON.stringify({
        data: 'Hello World!'
      }));
      console.log('Get finish');
    };
    
    export default Get;
    

    And I create the main index.ts file.

    // File: index.ts
    import http, { IncomingMessage, OutgoingMessage } from 'http';
    import Get from './get';
    
    const server = http.createServer((req: IncomingMessage, res: OutgoingMessage) => {
      console.log('Receiving IncomingMessage');
      res.setHeader('Content-Type', 'application/json');
      Get(req, res);
    });
    
    const port = 8000;
    server.listen(port);
    server.on('listening', () => {
      console.log(`Listen http on ${port}`);
    });
    

    I create the test file: get.spec.ts

    // File: get.spec.ts
    import sinon from 'sinon';
    import http, { OutgoingMessage } from 'http';
    import { Socket } from 'net';
    import { expect } from 'chai';
    import Get from './get';
    
    describe('GET', () => {
      it('should call end with the correct value', async () => {
        // I copy paste from your code with minor code style edit.
        const outgoingMessageSpy = sinon.spy(OutgoingMessage.prototype, 'end');
        const socket = new Socket();
        const incoming = new http.IncomingMessage(socket);
        const outgoing = new http.OutgoingMessage();
        // This is just some private property which need to be set.
        (outgoing as any)._header = true;
        // NOTE: If you want invalid response, remove the "await".
        await Get(incoming, outgoing);
    
        // Set the debug message here before expect.
        console.log('SPY Counter:', outgoingMessageSpy.callCount);
        console.log('SPY CalledOnce:', outgoingMessageSpy.calledOnce);
        expect(outgoingMessageSpy.calledOnce).to.be.true;
      });
    });
    

    When I run using ts-mocha from terminal, the result is like this:

    $ npx ts-mocha get.spec.ts
    
    
      GET
    Get start
    Get finish
    SPY Counter: 1
    SPY CalledOnce: true
        ✓ should call end with the correct value (204ms)
    
    
      1 passing (206ms)
    

    You see that you have setup the spy to OutgoingMessage.prototype.end correctly.

    BUT, when you remove await inside the test (see NOTE inside get.spec.ts file), this is the result:

    $ npx ts-mocha get.spec.ts
    
    
      GET
    Get start
    SPY Counter: 0
    SPY CalledOnce: false
        1) should call end with the correct value
    
    
      0 passing (8ms)
      1 failing
    
      1) GET
           should call end with the correct value:
    
          AssertionError: expected false to be true
          + expected - actual
    
          -false
          +true
    
    Get finish
    

    The condition is: end method get called correctly but after the it test has been evaluated.