了解单元测试的概念
- Node Test Helper(节点测试助手)
- Adding to your node project dependencies(添加到您的节点项目依赖项)
- Using a local Node-RED install for tests(使用本地 Node-RED 安装进行测试)
- Adding test script to package.json(将测试脚本添加到package.json)
- Creating unit tests(创建单元测试)
- Example unit test(示例单元测试)
- Initializing Helper(初始化助手)
- Getting nodes in the runtime(在运行时获取节点)
- Receiving messages from nodes(从节点接收消息)
- Working with Spies(与间谍合作)
- Running your tests(运行你的测试)
- Creating test flows with the editor(使用编辑器创建测试流程)
- Using catch and status nodes in test flows(在测试流程中使用catch和status节点)
- Additional examples(其他示例)
- API
- Running helper examples(运行帮助程序示例)
- Keywords
Node Test Helper(节点测试助手)
此测试助手模块使 Node-RED 核心的节点单元测试框架可供节点贡献者使用。
使用 test-helper,您的测试可以在启动 Node-RED 时、加载测试流,并接收消息以确保您的节点代码是正确的。
Adding to your node project dependencies(添加到您的节点项目依赖项)
助手需要 Node-RED 作为对等依赖项,这意味着它必须与助手本身一起安装。 要为您的节点项目创建单元测试,请添加此测试助手和 Node-RED,如下所示:
npm install node-red-node-test-helper node-red --save-dev
//node-red-node-test-helper 和node-red一起安装;
save-dev是添加依赖项;
这会将帮助模块添加到您的package.json文件中:
...
“ devDependencies ”:{
“ node-red ”:“ ^0.18.4 ”,
“ node-red-node-test-helper ”:“ ^0.1.8 ”
}
...
Using a local Node-RED install for tests(使用本地 Node-RED 安装进行测试)
如果您已经为开发安装了 Node-RED,则可以创建指向本地安装的符号链接。 例如,如果在 ~/projects 文件夹中克隆了 Node-RED,请使用:
(这是我们已经安装过node-red之后进行的操作,就不用进行第一个二级子标题的内容了;其中~/projects 文件夹指的是我们创建的项目所在的文件夹,比如说之前在桌面上创建了一个“四则运算”的项目,则应使用 F:\桌面\四则运算 文件夹。)
npm install ~/projects/node-red --no-save
//这是我们已经安装过node-red之后进行的操作,就不用进行第一个二级子标题的内容了;
Adding test script to package.json(将测试脚本添加到package.json)
要运行测试,您可以在脚本部分的 package.json 文件中添加一个测试脚本。 运行 test 目录中所有带有 _spec.js 前缀的文件,例如:
...
"scripts": {
"test": "mocha \"test/**/*_spec.js\""
},
...
这将允许您npm test在命令行上使用。
Creating unit tests(创建单元测试)
我们建议 将单元测试脚本放在项目的 test/ 文件夹中,并使用 *_spec.js(用于规范)后缀命名约定。
Example unit test(示例单元测试)
这是 Node-RED 文档中用于测试小写节点的示例测试。 这里我们将测试脚本命名为 test/lower-case_spec.js。
test/lower-case_spec.js:
var should = require("should");
var helper = require("node-red-node-test-helper");
var lowerNode = require("../lower-case.js");
helper.init(require.resolve('node-red'));
describe('lower-case Node', function () {
beforeEach(function (done) {
helper.startServer(done);
});
afterEach(function (done) {
helper.unload();
helper.stopServer(done);
});
it('should be loaded', function (done) {
var flow = [{ id: "n1", type: "lower-case", name: "lower-case" }];
helper.load(lowerNode, flow, function () {
var n1 = helper.getNode("n1");
try {
n1.should.have.property('name', 'lower-case');
done();
} catch(err) {
done(err);
}
});
});
it('should make payload lower case', function (done) {
var flow = [
{ id: "n1", type: "lower-case", name: "lower-case",wires:[["n2"]] },
{ id: "n2", type: "helper" }
];
helper.load(lowerNode, flow, function () {
var n2 = helper.getNode("n2");
var n1 = helper.getNode("n1");
n2.on("input", function (msg) {
try {
msg.should.have.property('payload', 'uppercase');
done();
} catch(err) {
done(err);
}
});
n1.receive({ payload: "UpperCase" });
});
});
});
在这个例子中,我们需要断言,这个帮助模块,以及我们要测试的小写节点,位于父目录中。
然后我们有一组 mocha 单元测试。 这些测试检查节点是否正确加载,并确保它使有效负载字符串按预期小写。
注意断言失败是如何被显式捕获并传递给 done() 调用的。 Node-RED 会吞下流中引发的异常,因此我们确保测试框架能够识别它们。 不这样做只会导致测试超时,因为在断言失败的情况下永远不会调用 done()。
Initializing Helper(初始化助手)
首先,我们需要告诉助手在哪里可以找到 node-red 运行时。 如图所示,这是通过调用 helper.init(require.resolve(‘node-red’)) 来完成的。
助手采用可选的 userSettings 参数,该参数与运行时默认值合并。
helper.init(require.resolve('node-red'), {
functionGlobalContext: { os:require('os') }
});
Getting nodes in the runtime(在运行时获取节点)
一旦 Node-RED 服务器和运行时准备就绪,异步 helper.load() 方法就会调用提供的回调函数。 然后我们可以调用 helper.getNode(id) 方法来获取运行时节点的引用。 有关这些方法的更多信息,请参阅下面的 API 部分。
Receiving messages from nodes(从节点接收消息)
第二个测试使用运行时中的helper节点连接到我们被测lower-case节点的输出。 helper节点是一个没有功能的模拟节点。 通过添加示例中的“输入”事件处理程序,我们可以检查 helper)接收到的消息。
要将消息发送到被测的lower-case节点 n1,我们在该节点上调用 n1.receive({ payload: “UpperCase” })。 然后,我们可以检查helper节点输入事件处理程序中的有效负载确实是小写的。
Working with Spies(与间谍合作)
间谍(文档)可帮助您收集有关函数被调用次数、调用内容、返回内容等的信息。
这个帮助程序库会自动为 Node.prototype 上的以下函数创建间谍(这些函数与“创建节点”指南中提到的函数相同):
trace()
debug()
warn()
log()
status()
send()
警告:不要试图用 sinon.spy()自己窥探这些函数; 既然已经是间谍,诗乃就会抛出异常!
Synchronous Example: Initialization(同步示例:初始化)
如果配置中不存在 (somethingGood),则 FooNode 节点将在初始化/构造时调用 warn(),如下所示:
// /path/to/foo-node.js
module.exports = function FooNode (config) {
RED.nodes.createNode(this, config);
if (!config.somethingGood) {
this.warn('badness');
}
}
然后你可以断言:
// /path/to/test/foo-node_spec.js
const FooNode = require('/path/to/foo-node');
it('should warn if the `somethingGood` prop is falsy', function (done) {
const flow = {
name: 'n1',
somethingGood: false,
/* ..etc.. */
};
helper.load(FooNode, flow, function () {
n1.warn.should.be.calledWithExactly('badness');
done();
});
});
Synchronous Example: Input(同步示例:输入)
当它接收到输入时,如果 msg.omg 为真,FooNode 将立即调用 error():
// somewhere in FooNode constructor
this.on('input', msg => {
if (msg.omg) {
this.error('lolwtf');
}
// ..etc..
});
以下是如何做出该断言的示例:
describe('if `omg` in input message', function () {
it('should call `error` with "lolwtf" ', function (done) {
const flow = {
name: 'n1',
/* ..etc.. */
};
helper.load(FooNode, flow, function () {
const n1 = helper.getNode('n1')
n1.receive({omg: true});
n1.on('input', () => {
n1.warn.should.be.calledWithExactly('lolwtf');
done();
});
});
});
});
Asynchronous Example(异步示例)
稍后在 FooNode 的input侦听器中,可能会异步调用 warn(),如下所示:
// somewhere in FooNode constructor function
this.on('input', msg => {
if (msg.omg) {
this.error('lolwtf');
}
// ..etc..
Promise.resolve()
.then(() => {
if (msg.somethingBadAndWeird) {
this.warn('bad weirdness');
}
});
});
前面示例中用于测试 msg.omg 行为的策略将不起作用! n1.warn.should.be.callWithExactly(‘bad weirdness’) 会抛出一个 AssertionError,因为 warn() 还没有被调用; EventEmitters 是同步的,测试的input监听器是在 FooNode 的函数中的input监听器完成之后直接调用的——但在 Promise 被解析之前!
由于我们不知道何时准确地调用 warn()(缺少使用 setTimeout 并等待 n 毫秒然后检查的缓慢、容易出现竞争条件的解决方案),我们需要一种不同的方法来检查调用。 神奇的是,这个帮助模块提供了一个解决方案。
当调用 warn 时,helper 将导致 FooNode 异步发出事件(以及上面列表中的其他方法)。 事件名称的格式为 call:; 在这种情况下,methodName 为 warn,因此事件名称为 call:warn。 该事件将传递一个参数:对应于最新方法调用的 Spy Call 对象 (docs)。 然后,您可以对这个 Spy Call 参数进行断言,如下所示:
describe('if `somethingBadAndWeird` in input msg', function () {
it('should call "warn" with "bad weirdness" ', function (done) {
const flow = {
name: 'n1',
/* ..etc.. */
};
helper.load(FooNode, flow, function () {
const n1 = helper.getNode('n1')
n1.receive({somethingBadAndWeird: true});
// because the emit happens asynchronously, this listener
// will be registered before `call:warn` is emitted.
n1.on('call:warn', call => {
call.should.be.calledWithExactly('bad weirdness');
done();
});
});
});
});
如您所见,看起来与同步解决方案非常相似; 唯一的区别是事件名称和断言目标。
注意:当且仅当我们尝试进行断言时对 spy 的同步调用仍然是最新的,“异步”策略也将起作用。 这可能会在重构时导致细微的错误,因此在选择使用哪种策略时要小心。
Running your tests(运行你的测试)
要运行您的测试:
npm test
产生以下输出(对于本例):
> red-contrib-lower-case@0.1.0 test /dev/work/node-red-contrib-lower-case
> mocha "test/**/*_spec.js"
lower-case Node
✓ should be loaded
✓ should make payload lower case
2 passing (50ms)
Creating test flows with the editor(使用编辑器创建测试流程)
要使用 Node-RED 编辑器创建测试流,请将测试流导出到剪贴板,然后将流粘贴到您的单元测试代码中。 以这种方式包含(helper)节点的一种有用技术是使用(debug)节点作为(helper)节点的占位符,然后搜索并将“type”:“debug”替换为“type”:“helper” 需要的地方。
Using catch and status nodes in test flows(在测试流程中使用catch和status节点)
要在测试流中使用 catch 和 status 或其他依赖于运行时特殊处理的节点,您通常需要添加一个选项卡来标识流,并将关联的 z 属性添加到您的节点以将节点与流相关联。 例如:
var flow = [{id:"f1", type:"tab", label:"Test flow"},
{ id: "n1", z:"f1", type: "lower-case", name: "test name",wires:[["n2"]] },
{ id: "n2", z:"f1", type: "helper" }
Additional examples(其他示例)
有关从 Node-RED 核心获取的其他测试示例,请参阅 test/examples 文件夹中提供的 .js 文件以及 Node-RED 存储库中 test/nodes 中的相关测试代码。
API
Work in progress.
load(testNode, testFlows, testCredentials, cb)(负载(testNode,testFlows,testCredentials,cb))
加载流程然后启动流程。此函数具有以下参数:
- testNode: (object|array of objects) 由 require 函数返回的待测试节点的模块对象。该节点将被注册,并且可以在 testFlows 中使用。
- testFlow:(对象数组)流数据以测试节点。如果要使用从 Node-RED 编辑器导出的流数据,请将流导出到剪贴板并将内容粘贴到测试脚本中。
- testCredentials:(对象)可选节点凭据。
- cb: (function) testFlows 启动时回调的函数。
unload() (卸下())
返回停止所有流程的承诺,清理测试运行时。
getNode(id) (获取节点(ID))
在 testFlow 中按 id 返回节点实例。可以检索在 testFlows 中定义的任何节点,包括添加到流中的任何辅助节点。
clearFlows() (清除流)
停止所有流程
request() (要求())
创建对编辑器/管理员 url 的 http(超级测试)请求。
例如:
helper.request().post('/inject/invalid').expect(404).end(done);
settings(userSettings) (设置(用户设置))
将任何 userSettings 与 RED.settings 返回的默认值合并。 每次调用此方法都会覆盖以前的 userSettings 以防止测试中出现意外问题。
这将使您能够在测试中复制生产环境,例如,您使用 functionGlobalContext 在函数中启用额外的节点模块。
// functions can now access os via global.get('os')
helper.settings({ functionGlobalContext: { os:require('os') } });
// reset back to defaults
helper.settings({ });
startServer(done) (启动服务器(完成))
启动一个 Node-RED 服务器,用于测试依赖于 http 或 web 套接字端点(如调试节点)的节点。 在所有测试用例之前启动 Node-RED 服务器:
before(function(done) {
helper.startServer(done);
});
stopServer(done) (停止服务器(完成))
停止服务器。 通常在 unload() 完成后调用。 例如,要卸载一个流,然后在每次测试后停止服务器:
url() (网址( ))
返回帮助服务器的 URL,包括启动服务器时使用的临时端口。
log() (日志( ))
var logEvents = helper.log().args.filter(function(evt {
return evt[0].type == "batch";
});
Running helper examples(运行帮助程序示例)
npm run examples
这会在包含的小写节点(如上所述)以及一些核心节点的 Javascript 文件的快照上运行测试,以确保帮助程序按预期工作。
Keywords
test ,iot ,node-red