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
?
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.