Capturing input messages with node-red-node-test-helper not working

Hi,

recently I started writing unit tests for my Node-RED nodes. I'm using node-red-node-test-helper and already implemented a couple of tests which work just fine.

But now I got stuck implementing a test which involves sending a message from my node to the helper node. I just cannot get it working, the callback function for the "input" event is never called. But I'm sure that my node sends a message during the test. I verified this by instrumenting the code and saw that the send function is called. Additionally I faked the test to pass and measured the coverage and also here I saw that the send function was called.

The code of the node under test is available on GitHub:

This is my test code:

const sinon = require("sinon");

const helper = require("node-red-node-test-helper");
const configNode = require("../nodes/config.js");
const schedulerNode = require("../nodes/scheduler.js");
const chronos = require("../nodes/common/chronos.js");
const moment = require("moment");

describe("scheduler node", function()
{
    context("node timers", function()
    {
        let clock = null;

        before(function(done)
        {
            helper.startServer(done);
        });

        after(function(done)
        {
            helper.stopServer(done);
        });

        beforeEach(function()
        {
            clock = sinon.useFakeTimers();
            sinon.stub(chronos, "getCurrentTime").returns(moment().utc());
        });

        afterEach(function()
        {
            helper.unload();
            sinon.restore();
            clock.restore();
        });

        it("should trigger at specified time", async function(done)
        {
            const flow = [{id: "sn1", type: "chronos-scheduler", name: "scheduler", config: "cn1", wires: [["hn1"]], schedule: [{trigger: {type: "time", value: "00:01", offset: 0, random: false}, output: {type: "msg", property: {name: "payload", type: "string", value: "test"}}}], outputs: 1},
                          {id: "hn1", type: "helper"},
                          {id: "cn1", type: "chronos-config", name: "config"}];
            const credentials = {"cn1": {latitude: "50", longitude: "10"}};

            await helper.load([schedulerNode, configNode], flow, credentials);
            const hn1 = helper.getNode("hn1");

            hn1.on("input", function(msg)
            {
                try
                {
                    msg.should.have.property("payload", "test");
                    done();
                }
                catch(e)
                {
                    done(e);
                }
            });

            clock.tick(60000);
        });
    });
});

I always get this error from mocha as the done function is never called:

Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.

Any idea what I'm doing wrong and how I can get this working?

Best regards,
Jens

Any help? I know that node-red-node-test-helper is not used so widely outside of the Node-RED core development, but maybe someone from the Node-RED development team has an idea? Could it be related to the usage of sinonjs fake timers? Or maybe because of using a configuration node and credentials? There do not seem to exist comparable examples from the node-red-node-test-helper repository or the Node-RED unit tests.

I'll try to take a look tonight. I can't see anything obvious from the code you've shared. As you note, there aren't comparable examples in the core tests - in particular, we`ve never used the fake timers provided by Sinon (although perhaps we should...).

This was happening to my code also (the Timeout of 2000ms exceeded) and I had to approach the timer test a bit differently.

I can't recall much about this as I'm currently working on other things. The main command:

  "scripts": {
    "pretest": "eslint *.js",
    "test": "mocha --exit \"test/*_spec.js\"",
     ...
  },

I think that 0000_basic_spec.js may have something interesting, Test Case 23 runs for 15 seconds. Just understand that I haven't completed all the tests but I think the basic test cases are all valid and correct.

I implemented another test without fake timers, this time checking the retrieval of an input message (which is independent of any timers). Also this test fails because the "input" event callback added in the test is never called. Again I instrumented the code of my node and could see that the message was received inside the node itself.

    context("node input", function()
    {
        let clock = null;

        before(function(done)
        {
            helper.startServer(done);
        });

        after(function(done)
        {
            helper.stopServer(done);
        });

        beforeEach(function()
        {
        });

        afterEach(function()
        {
            helper.unload();
        });

        it("should switch off schedule", async function(done)
        {
            const flow = [{id: "sn1", type: "chronos-scheduler", name: "scheduler", config: "cn1", schedule: [{trigger: {type: "time", value: "00:01", offset: 0, random: false}, output: {type: "msg", property: {name: "payload", type: "string", value: "test"}}}], outputs: 1},
                          {id: "cn1", type: "chronos-config", name: "config"}];
            const credentials = {"cn1": {latitude: "50", longitude: "10"}};

            await helper.load([configNode, schedulerNode], flow, credentials);
            const sn1 = helper.getNode("sn1");

            sn1.on("input", function()
            {
                try
                {
                    sn1.disabledSchedule.should.be.false();
                    done();
                }
                catch(e)
                {
                    done(e);
                }
            });

            sn1.receive({payload: false});
        });
    });

As there are no Sinon fake timers involved this time, the problem must be caused by something else. The only difference compared to all the examples from node-red-node-test-helper and Node-RED unit tests I can see is the usage of a configuration node and the usage of credentials.

Regarding the above case, after walking through the code with a debugger, I found out that the fact of calling the done function inside of the input event listener of my node seems to prevent the second input event listener (defined in my test function) to be called. This is because there is a variable c inside the _emitInput function (the handler of the input event) which is decreased inside the done callback and therefore the for-loop is left after the node's input event listener.

Now I'm wondering how I could rewrite my test code so that I can still have some code which runs after the input event handler of my node and checks some post conditions.

Finally I solved my initial problem. This was indeed caused by the SinonJS fake timers, more precisely the faking of setImmediate. This function is used inside Node-RED runtime to schedule the sending of the message on the next iteration of the event loop and due to the fake, this somehow didn't work then. So by only faking the functions relevant for my test (Date, setTimeout and clearTimeout), I got it working. I still had to do a small modification in the test function itself because due to faking setTimeout, the callback and hence the execution of the test input event listener happens synchronously inside the clock.tick method call.

So here is the code that is now working:

const sinon = require("sinon");

const helper = require("node-red-node-test-helper");
const configNode = require("../nodes/config.js");
const schedulerNode = require("../nodes/scheduler.js");
const chronos = require("../nodes/common/chronos.js");
const moment = require("moment");

describe("scheduler node", function()
{
    context("node timers", function()
    {
        let clock = null;

        before(function(done)
        {
            helper.startServer(done);
        });

        after(function(done)
        {
            helper.stopServer(done);
        });

        beforeEach(function()
        {
            clock = sinon.useFakeTimers({toFake: ["Date", "setTimeout", "clearTimeout"]});
            sinon.stub(chronos, "getCurrentTime").returns(moment().utc());
        });

        afterEach(function()
        {
            helper.unload();
            sinon.restore();
            clock.restore();
        });

        it("should trigger at specified time", async function()
        {
            const flow = [{id: "sn1", type: "chronos-scheduler", name: "scheduler", config: "cn1", wires: [["hn1"]], schedule: [{trigger: {type: "time", value: "00:01", offset: 0, random: false}, output: {type: "msg", property: {name: "payload", type: "string", value: "test"}}}], outputs: 1},
                          {id: "hn1", type: "helper"},
                          {id: "cn1", type: "chronos-config", name: "config"}];
            const credentials = {"cn1": {latitude: "50", longitude: "10"}};

            await helper.load([schedulerNode, configNode], flow, credentials);
            const hn1 = helper.getNode("hn1");

            hn1.on("input", function(msg)
            {
                msg.should.have.property("payload", "test");
            });

            clock.tick(60000);
        });
    });
});

However, I still have the other problem from my two posts before. Maybe anybody knows how to solve this one, thanks!

Best regards
Jens

1 Like

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.