Need help debugging a unittest

I build a custom node and want to unittest it using node-red-node-test-helper.

I first make a custom flow in Node-RED and exported it. Then I follow the examples to turn that flow json object into a pure node array. Finally, I use helper routines to load the head and the tail node for input and output. But helper.getNode(headID) and helper.getNode(tailID) keep giving me null.
I stepped into the getNode() but found that my activeFlows is empty.

I can't figure out why since I assume the helper can run a defined node list literal without a problem. Here is my test routine:

async function testFlow(nodeRuntime, helper, wiredNodes, inputMsg, expectation) {
// usage:
// - nodeRuntime: require(service/node/node.js)
// - wiredNodes: simplified node list from standard Node-RED flow json object
//   - e.g., [
//       {id: "my.node.id", type: "service_a", "wires": ["next.node.id", ...]},
//       {id: "next.node.id", type: "service_b", "wires": ["3rd.node.id", ...]},
//       ...
//     ]
// - inputMsg: Node-RED message fed to the first node of the flow
//   - e.g., { topic: "my.topic", payload: obj, miaservice: "service_name" }
// - expectation: assertion wrapper; can be one of:
//   - (outMsg) => { expect(outMsg).to....(); }
//   - (outMsg) => { expect(customCondition()).to...(); }
//   e.g., if your flow has no output, but writes to a file, then simply test then file content and ignore the outMsg argument
	let tail = wiredNodes.slice(-1)[0];
	// lazy-append helper to user flow 
	let tailID = "flowtest.tail";
	let helperName = 'helper';
	let userAddedHelperAsTail = tail.type === helperName;
	if ( !userAddedHelperAsTail ) {
	    // assume: tail has only one flow path,
		// wire up help node as new tail
		wiredNodes.slice(-1)[0].wires[0].push(tailID);
		wiredNodes.push({id: tailID, type: helperName});
	}
	await helper.load(nodeRuntime, wiredNodes);
	var tailNode = helper.getNode(tailID);
	var headNode = helper.getNode(wiredNodes[0].id);
        #
        # expected headNode to be valid, but got null! 
        #
	headNode.receive(inputMsg);
	await asyncAssertFlowOutput(tailNode, expectation);
};

Using this routine, in the debugger I can see that my wiredNodes are all valid according to helper's user guide: They all have id, type, wires, and other custom fields.

Help is much appreciated!

wires is an array of arrays [ [] ]

See any of the test in any src e.g...

hmm, that's a good catch, but I believe it's a doc error on my part. The implementation notices that the wires is an array of array, e.g.,

// assume: tail has only one flow path,
// wire up help node as new tail
wiredNodes.slice(-1)[0].wires[0].push(tailID);

here I lazy-append a helper node to the node's first wire-array.

what other nodes are in your wiredNodes argument other than service_x types?

@Steve-Mcl
Here is the flow i'm trying to load:

[
    {
        "id": "a7a60eb01af1bc26",
        "type": "tab",
        "label": "Flow 2",
        "disabled": false,
        "info": ""
    },
    {
        "id": "32e5afc70baae91f",
        "type": "inject",
        "z": "a7a60eb01af1bc26",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            },
            {
                "p": "my_service_type",
                "v": "inject",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "inject",
        "payload": "{\"pathlist\":\"roots.list\"}",
        "payloadType": "json",
        "x": 550,
        "y": 140,
        "wires": [
            [
                "70c49ab97918cdb2"
            ]
        ]
    },
    {
        "id": "70c49ab97918cdb2",
        "type": "find_files_folders",
        "z": "a7a60eb01af1bc26",
        "rootlist": "",
        "includes": [
            "*.js"
        ],
        "excludes": [],
        "caseinsensitive": false,
        "useregex": false,
        "targettype": "file",
        "_pathlist": true,
        "saveconfig": true,
        "cfgfile": "",
        "dryrun": false,
        "x": 746,
        "y": 140,
        "wires": [
            []
        ]
    }
]

I would slim that down to bare minimum. Remove tab nodes, remove x, y, and z and properties

e.g...

[
    {
        "id": "32e5afc70baae91f",
        "type": "inject",
        "name": "",
        "props": [ {     "p": "payload" },
            {"p": "topic","vt": "str" },
            { "p": "my_service_type", "v": "inject", "vt": "str" }
        ],
        "topic": "inject",
        "payload": "{\"pathlist\":\"roots.list\"}",
        "payloadType": "json",
        "wires": [ [ "70c49ab97918cdb2" ] ]
    },
    {
        "id": "70c49ab97918cdb2",
        "type": "find_files_folders",
        "rootlist": "",
        "includes": [ "*.js" ],
        "excludes": [],
        "caseinsensitive": false,
        "useregex": false,
        "targettype": "file",
        "_pathlist": true,
        "saveconfig": true,
        "cfgfile": "",
        "dryrun": false,
        "wires": [ [] ]
    }
]

Q: I assume find_files_folders is one of your custom nodes and is required and passed in via nodeRuntime arg?

Also, depending on your env it might be necessary to pass in the inject node e.g. require the inject node and pass it along with your custom node, as an array to helper.load in the first arg.

Also, depending on your env it might be necessary to pass in the inject node e.g. require the inject node and pass it along with your custom node, as an array to helper.load in the first arg.

OK. That sounds like the issue. I didn't realize I needed to explicitly load the runtime of the buildin nodes. But come to think of it, the helper node itself was requireed, so it makes sense.

Just tried an naive loading like this

let injectRuntime = require("D:\\Downloads\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\20-inject.js");
await helper.load(injectRuntime, wiredNodes);
 await helper.load(nodeRuntime, wiredNodes);

and I got the error

 TypeError: Attempted to wrap log which is already wrapped

      113 |     let injectRuntime = require("D:\\Downloads\\node-red\\packages\\node_modules\\@node-red\\nodes\\core\\common\\20-inject.js");
      114 |     await helper.load(injectRuntime, wiredNodes);
    > 115 |     await helper.load(nodeRuntime, wiredNodes);

I also tried to follow your linked example

var fileNode = require("nr-test-utils").require("@node-red/nodes/core/storage/10-file.js");

Which gives me

TypeError: Attempted to wrap log which is already wrapped

      114 |     injectRuntime = require('D:\\Downloads\\node-red\\test\\node_modules\\nr-test-utils').require('@node-red\\nodes\\core\\common\\20-inject.js');
      115 |     await helper.load(injectRuntime, wiredNodes);
    > 116 |     await helper.load(nodeRuntime, wiredNodes);

As for removing extra properties, I was successful the other day loading a flow that includes only one node (my own node). That exported flow has the whole bag of extra properries including the x/y/z.

Why are you calling helper.load twice?

e.g...

await helper.load([nodeRuntime, injectRuntime], wiredNodes)

Sorry. I missed that detail.

	let injectRuntime = require('D:\\Downloads\\node-red\\test\\node_modules\\nr-test-utils').require('@node-red\\nodes\\core\\common\\20-inject.js');
	await helper.load([injectRuntime, nodeRuntime], wiredNodes);

This time the loading seems to get stuck:

    thrown: "Exceeded timeout of 5000 ms for a test.
    Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."

      78 |     defineFlowTest(description, wiredNodes, inputMsg, expectation, timeoutMs=5000) {
      79 |     // help: test a flow using its simplified json object
    > 80 |      it(description, async () => {
         |      ^
      81 |              await testFlow(this.nodeRuntime, this.helper, wiredNodes, inputMsg, expectation);
      82 |      }, timeoutMs);
      83 |     }

Not sure if my way of loading the inject node runtime is correct.

Finally got this problem resolved. It was my own bugs somewhere in the node implementation.

Previously, the biggest confusion came from JEST's error message on async calls. JEST only shows meaningful results about its assertions. ANY OTHER errors before reaching its assertions will be reported as timeouts with the message:

"Exceeded timeout of 5000 ms for a test.
    Use jest.setTimeout(newTimeout) to increase the timeout value, if this is a long-running test."

This message is essentially JEST saying "I was unable to reach any expect().toBe() and errored out". So any syntax error or exception counts, but they won't show up in the log! So you are on your own to debug the entire test at this point.

I must apologize for waisting time of others for looking at this.
But at least now I won't get distracted by the false log.

1 Like

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