Sinon spy.called no working
Background
I have a small server that receives data from a machine. Every time I get a message, I call a function on the dispatcher object that just console.log
gets everything.
Problem
The code works well as I can see console.log
in the console, but Sinon spy.called
doesn't work. It is always false
no matter how many times I call dispatcher.onMessage
.
code
server.js
const eventDispatcher = {
onMessage: console.log,
};
const server = (dispatcher = eventDispatcher) => {
//this gets called everytime the server receives a message
const onData = data => {
//Process data
//....
dispatcher.onMessage(data);
};
const getDispatcher = () => dispatcher;
return Object.freeze({
getDispatcher
});
};
test.js
describe("message sender", () => {
const myServer = serverFactory();
it("should send information to server", () => {
dummyMachine.socket.write("Hello World!\r\n");
const dataSpy = sinon.spy(myServer.getDispatcher(), "onMessage");
expect(dataSpy.called).to.be.true; //always fails!
});
});
Study
After reading posts like this, I believe this is due to some layer of indirection as stated in:
And you need to fix it with this
:
However, looking at my code, I really can't get what I'm missing.
Question
- What am I doing wrong?
MCVE
Directory structure
Project_Folder
|____package.json
|____server.js
|____test
|____ dummyMachine_spec.js
package.json
{
"name": "sinon-question",
"version": "1.0.0",
"description": "MCVE about a dummy machine connecting to a server for StackOverflow",
"main": "server.js",
"scripts": {
"test": "NODE_ENV=test mocha --reporter spec --slow 5000 --timeout 5000 test/*_spec.js || true"
},
"author": "Pedro Miguel P. S. Martins",
"license": "ISC",
"devDependencies": {
"chai": "^3.5.0",
"mocha": "^3.3.0",
"sinon": "^2.2.0"
},
"dependencies": {
"net": "^1.0.2"
}
}
server.js
"use strict";
const net = require("net");
const eventDispatcher = {
onMessage: console.log,
};
const server = (dispatcher = eventDispatcher) => {
let serverSocket;
const onData = data => {
//Process data
dispatcher.onMessage(`I am server and I got ${data}`);
};
const start = (connectOpts) => {
return new Promise(fulfil => {
serverSocket = net.createConnection(connectOpts, () => {
serverSocket.on("data", onData);
fulfil();
});
});
};
const stop = () => serverSocket.destroy();
const getDispatcher = () => dispatcher;
return Object.freeze({
start,
stop,
getDispatcher
});
};
module.exports = server;
Test / dummyMachine.js
"use strict";
const chai = require("chai"),
expect = chai.expect;
const sinon = require("sinon");
const net = require("net");
const serverFactory = require("../server.js");
describe("Dummy Machine", () => {
const dummyMachine = {
IP: "localhost",
port: 4002,
server: undefined,
socket: undefined
};
const server = serverFactory();
before("Sets up dummyReader and server", done => {
dummyMachine.server = net.createServer(undefined, socket => {
dummyMachine.socket = socket;
});
dummyMachine.server.listen(
dummyMachine.port,
dummyMachine.IP,
undefined,
() => {
server.start({
host: "localhost",
port: 4002
})
.then(done);
}
);
});
after("Kills dummyReader and server", () => {
server.stop();
dummyMachine.server.close();
});
it("should connect to server", done => {
dummyMachine.server.getConnections((err, count) => {
expect(err).to.be.null;
expect(count).to.eql(1);
done();
});
});
it("should send information to server", () => {
dummyMachine.socket.write("Hello World\r\n");
const dataSpy = sinon.spy(server.getDispatcher(), "onMessage");
expect(dataSpy.called).to.be.true; //WORK DAAMN YOU!
});
});
Instructions for MCVE
- Download the files and create the specified directory structure.
- Enter your project folder and type
npm install
in terminal - A type
npm test
The first test must pass, which means the connection is actually being made.
The second test will fail even if you get a console log confirming that it was called onMessage
.
source to share
The main problem is that it is not enough just to track onMessage
, because your test will never know exactly when it received the call (because the thread events are delivered asynchronously).
You can use the hack with help setTimeout()
, and check and see if it is triggered some time after the message is sent to the server, but this is not ideal.
Instead, you can replace onMessage
with a function to be called instead, and from that function you can check and see if the correct arguments were called, etc.
Sinon provides stubs that can be used for this:
it("should send information to server", done => {
const stub = sinon.stub(server.getDispatcher(), 'onMessage').callsFake(data => {
stub.restore();
expect(data).to.equal('I am server and I got Hello World\r\n');
done();
});
dummyMachine.socket.write("Hello World\r\n");
});
Instead of the original, onMessage
it will call the "fake function" you provide. There the stub is restored (meaning it is onMessage
restored to the original), and you can check and see if it has called the correct argument.
Since the test is asynchronous, it uses done
.
There are several things to consider:
- You cannot easily find that, due to a programming error,
onMessage
does not trigger the call at all. When this happens, Mocha will delay your test after a few seconds, causing it to fail. - If this happens, the stub will not be restored to its original, and any subsequent tests that try to stub
onMessage
will fail because the method is already shaded (usually you work around this by creating a stub inonBeforeEach
and restoring it inonAfterEach
) - This solution will not check the inner workings
onMessage
as it is being replaced. It only checks if it will be called and with the correct argument (however, it is better and easier to testonMessage
separately, by calling it directly with different arguments from your test cases).
source to share
I would guess the problem is caused by using the Object.freeze
object you would like to keep track of.
In most cases, these "spy" methods work by overwriting the spy function with another function that implements the "spy" functionality (for example: tracking function calls).
But if you freeze the object, the function cannot be overwritten.
source to share