Background
I am creating a dummy server using net from Node.js in my Mocha test.
I have a dummy test, and I want to start the server before the test starts, and kill it after:
"use strict";
/*global describe, it, expect, before, after*/
const net = require("net");
describe("dummy server test", () => {
const dummyReader = {
IP: "localhost",
port: 4002,
server: undefined,
socket: undefined
};
before("Starts dummy server", done => {
dummyReader.server = net.createServer(socket => {
dummyReader.socket = socket;
done();
});
dummyReader.server.listen(dummyReader.IP, dummyReader.port);
});
after("Kills dummy server", done => {
dummyReader.server.close();
dummyReader.socket.destroy();
done();
});
it("should pass", () => {
expect(true).to.be.true;
});
});
Problem
The problem is that my async before hook never completes. For a reason I can't understand done is never called, and thus the hook times out.
I tried increasing the time out, believing it could fix the issue, but to no avail.
Question
How can I fix my code?
There are two problems with your code:
You need to flip the host address and port arguments in dummyReader.server.listen(...);. The port comes first, and the host second.
The callback to net.createServer won't be called until something actually connects to the server, but you have nothing connecting to it.
With the following before hook the code will run. I've added code that creates a connection right away for illustration purposes.
before("Starts dummy server", done => {
dummyReader.server = net.createServer(socket => {
dummyReader.socket = socket;
done();
});
dummyReader.server.listen(dummyReader.port,
dummyReader.IP,
undefined,
() => {
// For illustration purposes,
// create a connection as soon
// as the server is listening.
net.connect(
dummyReader.port,
dummyReader.IP);
});
});
Seems to me though that what you should be doing is ending the before hook as soon as the server is listening and then connect to it in your tests. Here's an illustration of how it can be done:
"use strict";
/*global describe, it, expect, before, after*/
const net = require("net");
describe("dummy server test", () => {
const dummyReader = {
IP: "localhost",
port: 6002,
server: undefined,
socket: undefined
};
before("Starts dummy server", done => {
dummyReader.server = net.createServer(socket => {
console.log("got a new socket!");
dummyReader.socket = socket;
});
dummyReader.server.listen(dummyReader.port,
dummyReader.IP,
undefined,
() => {
done();
});
});
after("Kills dummy server", done => {
dummyReader.server.close();
// dummyReader.socket.destroy();
done();
});
let prevSocket;
it("should pass", (done) => {
net.connect(dummyReader.port, dummyReader.IP, () => {
console.log(dummyReader.socket.address());
prevSocket = dummyReader.socket;
done();
});
});
it("should pass 2", (done) => {
net.connect(dummyReader.port, dummyReader.IP, () => {
console.log(dummyReader.socket.address());
console.log("same socket?",
prevSocket === dummyReader.socket);
done();
});
});
});
Each time you connect, a new net.Socket object is created and assigned to dummyReader.socket and so you can access it from inside you test if needed. I've peppered the code with console.log statements to show some key values. When I run it here, I get:
dummy server test
got a new socket!
{ address: '127.0.0.1', family: 'IPv4', port: 6002 }
✓ should pass
got a new socket!
{ address: '127.0.0.1', family: 'IPv4', port: 6002 }
same socket? false
✓ should pass 2
2 passing (71ms)
Related
I have inplemented MQTT well but I am experiencing an issue with unsubscribe method. All other functions (mqttCon.publish() , mqttCon.subscribe(), mqttCon.on()...) are working well except this. I have checked the spelling and even done ctrl +click and it takes me to the library implementation meaning it is the right method and referenced well yet I keep getting the error below. How can I solve it?
This is the line: mqttCon.unsubscribe(topic)
TypeError: mqttCon.unsubscribe is not a function
at noopHandler (/home/dev/project-dir/src/mqtt/processMessage.js:5:13)
at module.exports (/home/dev/project-dir/src/mqtt/processMessage.js:10:20)
at MqttClient.client.on (/home/dev/project-dir/src/mqtt/mqttCon.js:16:13)
at MqttClient.emit (events.js:189:13)
at MqttClient._handlePublish (/home/dev/project-dir/node_modules/mqtt/lib/client.js:1271:12)
at MqttClient._handlePacket (/home/dev/project-dir/node_modules/mqtt/lib/client.js:410:12)
at work (/home/dev/project-dir/node_modules/mqtt/lib/client.js:321:12)
at Writable.writable._write (/home/dev/project-dir/node_modules/mqtt/lib/client.js:335:5)
at doWrite (/home/dev/project-dir/node_modules/mqtt/node_modules/readable-stream/lib/_stream_writable.js:428:64)
at writeOrBuffer (/home/dev/project-dir/node_modules/mqtt/node_modules/readable-stream/lib/_stream_writable.js:417:5)
NOTE: I am using ES6(Emacscript 6+) javascript and not Typescript.
Nodejs 12.18.1 and npm 6.14.6
Here is my connection code mqttCon.js:
const mqtt = require('mqtt')
const processMessage = require('./processMessage')
const logger = require('../logConf')
const options = {
host: '',
port: '',
username:'',
password: '',
protocol: ''
};
const client = mqtt.connect(options)
client.on("connect", function () {
console.log("MQTT connected with status: " + client.connected);
if (client.connected) {
client.on('message', (topic, message) => {
processMessage(topic, String(message))
})
}
})
client.on('error', error => {
console.log(error,'ERROR')
logger.errorLogger.error(error)
})
client.on('reconnect', error => {
console.log(error,'RECONNECT')
logger.errorLogger.error(error)
})
client.on('close', error => {
console.log(error,'CLOSE')
logger.errorLogger.error(error)
})
client.on('disconnect', error => {
console.log(error,'DISCONNECT')
logger.errorLogger.error(error)
})
client.on('offline', error => {
console.log(error,'OFFLINE')
logger.errorLogger.error(error)
})
module.exports = client
This is the processMessage.js :
const mqttCon = require('./mqttCon')
const logger = require('../logConf')
let noopHandler = (topic, message) => {
console.log(String(message))
mqttCon.unsubscribe(topic) //THIS IS WHERE THE ERROR IS OCCURRING *******************
}
module.exports = (topic, message) => {
switch (topic) {
case 'NOOOOOOOOOOOOOOOOOOOOOOOOP':
return noopHandler(topic, message)
case 'anotherTopic':
// return handleAnotherTopic(String(message))
return
default:
logger.errorLogger.error(new Error(`No handler for topic ${topic}`))
}
}
Your mqttCon.js file has no client.prototype.unsubscribe = function() {}, so the error is correct. You are defining client as a module, but you are really needing to call mqtt.unsubscribe() somewhere. So you need to either add an unsubscribe() function to the client constant (which really should be a var in this case), or call the mqtt.unsubscribe() function after requiring the mqtt module in your processMessage.js file....which I think goes against what you are trying to do anyway. You might want to read up a bit more about how module.exports actually works: https://www.sitepoint.com/understanding-module-exports-exports-node-js/
UPDATE:
The above influenced my thoughts and the issue was that I was importing processMessage.js which inturn imports mqttCon.js which imported it. Circular import, meaning mqttCon(mqttClient) was always not yet initialized inside processMessage.js. The solution was that I imported processMessage.js inside client.on('connect'....)... block when client is already initialized and exported well as a module like below:
client.on("connect", () => {
console.log("MQTT connected with status: " + client.connected);
if (client.connected) {
client.on('message', (topic, message) => {
require('./processMessage')(topic, String(message))
})
}
})
Getting metric has already been registered when trying to publish metrics from service. To avoid that I used register.removeSingleMetric("newMetric"); but problem is that it clear register and past records every time a new call comes in.
Wondering what to address this problem:
export function workController(req: Request, res: Response) {
resolveFeaturePromises(featurePromises).then(featureAggregate => {
return rulesService.runWorkingRulesForHook(featureAggregate, hook.name, headers)
.then(([shouldLog, result]) => {
...
// publish metric
publishHookMetrics(result);
// publish metric
sendResponse(res, new CoreResponse(result.isWorking, result.responseData), req, featureAggregate, hook);
});
}).catch(err => {
sendResponse(res, new CoreResponse(Action.ALLOW, null), req, null, hook);
console.error(err);
});
}
function publishCustomMetrics(customInfoObject: CustomInfoObject) {
const counter = new promClient.Counter({
name: "newMetric",
help: "metric for custom detail",
labelNames: ["name", "isWorking"]
});
counter.inc({
name: customInfoObject.hook,
isWorking: customInfoObject.isWorking
});
}
stack trace
[Node] [2020-07-30T17:40:09+0500] [ERROR] Error: A metric with the name newMetric has already been registered.
application.ts
export async function startWebServer(): Promise<Server> {
if (!isGlobalsInitilized) {
throw new Error("Globals are noit initilized. Run initGlobals() first.");
}
// Setup prom express middleware
const metricsMiddleware = promBundle({
includeMethod: true,
includePath: true,
metricsPath: "/prometheus",
promClient: {
collectDefaultMetrics: {
}
}
});
// start http server
const app = require("express")();
app.use(bodyParser.json());
app.use(metricsMiddleware);
const routeConfig = require("./config/route-config");
routeConfig.configure(app);
const port = process.env.PORT || 3000;
return app.listen(port, function () {
console.log("service listening on port", port);
});
}
versions:
express-prom-bundle: 6.0.0
prom-client: 12.0.0
The counter should be initialize only once, then used for each call. Simplest modification from code you gave would be something like this.
const counter = new promClient.Counter({
name: "newMetric",
help: "metric for custom detail",
labelNames: ["name", "isWorking"]
});
export function publishCustomMetrics(customInfoObject: CustomInfoObject) {
counter.inc({
name: customInfoObject.hook,
isWorking: customInfoObject.isWorking
});
}
BTW, I found this thread while searching a way to avoid the "already been registered" error in unit tests. Many tests was instantiating the same class (from a library) that was initializing a counter in the constructor.
As it is in tests, and I didn't need consistency over the metrics, I found an easy solution is to clear metrics registers at the beginning of each test.
import { register } from "prom-client";
// ...
register.clear();
I have defined beforAll and afterAll in a separate file bootstrap.js but I am not able to do integration testing. I am using serverless stack. I took help from github but that example was written in mocha so I tried to transform it to jest.
bootstrap.js
beforeAll(async () => {
console.log('[Tests Bootstrap] Start');
await startSlsOffline((err) => {
if (err) {
console.log(err);
}
console.log('[Tests Bootstrap] Done');
});
}, 30000);
afterAll(async () => {
console.log('[Tests Teardown] Start');
await stopSlsOffline();
console.log('[Tests Teardown] Done');
});
handler.test.js
describe('get Endpoints', () => {
const server = request(`http://localhost:3005`);
test('should run get example', async () => {
const res = await server.get('/example');
console.log('res', res.body);
});
});
My jest configuration is
module.exports = {
verbose: true,
bail: true,
coverageDirectory: 'output/coverage/jest',
setupFilesAfterEnv: [ './bootstrap.js' ]
};
The output I get is
> jest --config test/jest.config.js
FAIL test/handler.test.js
get Endpoints
✕ should run get example (38ms)
● get Endpoints › should run get example
connect ECONNREFUSED 127.0.0.1:3005
console.log test/bootstrap.js:6
[Tests Bootstrap] Start
console.log test/bootstrap.js:30
Serverless: Offline started with PID : 5587 and PORT: 3005
console.log test/bootstrap.js:18
[Tests Teardown] Start
console.log test/bootstrap.js:47
Serverless Offline stopped
console.log test/bootstrap.js:22
[Tests Teardown] Done
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.825s
Ran all test suites.
npm ERR! Test failed. See above for more details.
The global setup doesn't work the way you are expecting it to work. If you see the logs, your beforeAll logs are coming after your test executes. You should use different way to setup and teadown. Jest has concept of globalSetup and globalTeardown and I guess that fits better in your case. As part of this you can start and stop your server. The config will look like this
Read more here - https://jestjs.io/docs/en/configuration#globalsetup-string
module.exports = {
verbose: true,
bail: true,
coverageDirectory: 'output/coverage/jest',
globalSetup: "./bootstrap.js",
globalTeardown: "./bootstrap.js"
};
And your bootstrap will looks like this
const { spawn} = require('child_process');
let slsOfflineProcess;
module.exports = async () => {
console.log('[Tests Bootstrap] Start');
await startSlsOffline((err) => {
if (err) {
console.log(err);
}
console.log('[Tests Bootstrap] Done');
});
}
const startSlsOffline = (done) => {
if (slsOfflineProcess) {
slsOfflineProcess.kill('SIGINT');
console.log('Serverless Offline stopped');
done();
}
slsOfflineProcess = spawn('sls', [ 'offline', 'start', '--port', 3005 ]);
console.log(`Serverless: Offline started with PID : ${slsOfflineProcess.pid} and PORT: 3005`);
slsOfflineProcess.stdout.on('data', (data) => {
if (data.includes('Offline listening on')) {
console.log(data.toString().trim());
done();
}
});
slsOfflineProcess.stderr.on('data', (errData) => {
console.log(`Error starting Serverless Offline:\n${errData}`);
done(errData);
});
};
I solved my issue using this config with help of 1st answer using globalSetup and globalTeardown.
Read more here - https://jestjs.io/docs/en/configuration#globalsetup-string
bootstrap.js
const { spawn } = require('child_process');
let slsOfflineProcess;
module.exports = async () => {
console.log('[Tests Bootstrap] Start');
await startSlsOffline().catch((e) => {
console.error(e);
return;
});
global.__SERVERD__ = slsOfflineProcess;
};
function startSlsOffline() {
slsOfflineProcess = slsOfflineProcess = spawn('sls', [ 'offline', 'start', '--port', 3005 ]);
return finishLoading();
}
const finishLoading = () =>
new Promise((resolve, reject) => {
slsOfflineProcess.stdout.on('data', (data) => {
if (data.includes('Serverless: Offline [HTTP] listening on')) {
console.log(data.toString().trim());
console.log(`Serverless: Offline started with PID : ${slsOfflineProcess.pid}`);
resolve('ok');
}
if (data.includes('address already in use')) {
reject(data.toString().trim());
}
});
slsOfflineProcess.stderr.on('data', (errData) => {
console.log(`Error starting Serverless Offline:\n${errData}`);
reject(errData);
});
});
teardown.js
module.exports = async function() {
let slsOfflineProcess = global.__SERVERD__;
slsOfflineProcess.stdin.write('q\n');
slsOfflineProcess.stdin.pause();
await slsOfflineProcess.kill('SIGINT');
console.log('Serverless Offline stopped');
};
Find a sample on this link here: https://github.com/bilalsha/sls-test-jest
P.S globalTeardown do not works if test fails. I will post solution once I have it.
I am trying to test a WebSockets server. The onMessage handler gets called for the first time only - any further messages published to the server are not logged. Why is this happening?
`
const WebSocket = require('ws');
const ws = new WebSocket(WEBSOCKETS_ENDPOINT);
describe('When a speed test is triggered', () => {
test('Then AWS should send the result to the client', done => {
ws.on('open', async () => {
ws.send(JSON.stringify({
message: 'start-speed-test-rbsid',
data: RBSID,
platform: "android"
}));
ws.on('message', data => {
data = JSON.parse(data)
// this gets called only once - any further messages published from the server are not logged
console.log(data)
});
})
})
afterAll(() => ws.close())
});
Message data should be logged on every message that was published
I have no problem using you code against echo server.
const WebSocket = require('ws');
const ws = new WebSocket('wss://echo.websocket.org');
describe('When a speed test is triggered', () => {
test('Then AWS should send the result to the client', done => {
ws.on('open', async () => {
ws.send(JSON.stringify({
message: 'start-speed-test-rbsid',
data: 'RBSID',
platform: "android"
}));
ws.on('message', data => {
// this gets called only once - any further messages published from the server are not logged
console.log(data)
});
ws.send(JSON.stringify({
message: 'start-speed-test-rbsid',
data: 'RBSID',
platform: "android"
}));
})
})
afterAll(() => ws.close());
});
Another thing is, if you are writing unit test, and the test involves network IO, it is usually suggested to mock the network component.
const WebSocket = require('ws');
jest.mock('ws', () => {
class MockedWebSocket {}
return MockedWebSocket;
});
console.log(WebSocket); // mocked
Background
I have a small server that receives data from a machine. Every time I receive a message I call a function in a dispatcher object that simply console.logs everything it receives.
Problem
The code works well as I can see the console.logs 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!
});
});
Research
After reading similar posts I believe this happens due to some layer of indirection, as pointed in:
Sinon Spy is not called if the spied method is called indirectly
And should be fixed via using this:
Sinon.spy on a method is not invoked
However, looking at my code I really can't get what I am 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 directory structure indicated.
Enter project folder and type npm install on a terminal
Type npm test
The first test should pass, meaning a connection is in fact being made.
The second test will fail, even though you get the console log, proving that onMessage was called.
The main problem is that it's not enough to just spy on onMessage, because your test will never find out when it got called exactly (because stream events are asynchronously delivered).
You can use a hack using setTimeout(), and check and see if it got called some time after sending a message to the server, but that's not ideal.
Instead, you can replace onMessage with a function that will get called instead, and from that function, you can test and see if it got called with the correct arguments, etc.
Sinon provides stubs which 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" that you provide. There, the stub is restored (which means that onMessage is restored to its original), and you can check and see if it got called with the correct argument.
Because the test is asynchronous, it's using done.
There are a few things to consider:
You can't easily detect if, because of a programming error, onMessage doesn't get called at all. When that happens, after a few seconds, Mocha will timeout your test, causing it to fail.
If that happens, the stub won't be restored to its original and any subsequent tests that try to stub onMessage will fail because the method is already stubbed (usually, you work around this by creating the stub in an onBeforeEach and restore it in an onAfterEach)
This solution won't test the inner workings of onMessage, because it's being replaced. It only tests if it gets called, and with the correct argument (however, it's better, and easier, to test onMessage separately, by directly calling it with various arguments from your test cases).
I would guess that the problem is caused by using Object.freeze on the object that you would like to spy on.
Most of the time these "spy on" techniques work by overwriting the spied upon function with another function that implements the "spying" functionality (eg: keeps track of the function calls).
But if you're freezing the object, then the function cannot be overwritten.