Skip to content

Commit c938fde

Browse files
committed
improve async testing experience
- adds ability to listen for calls to `Node` methods by way of events - adds automatic spying on `Node` methods - fix issues with finding runtime path in certain environments - fix path-related portability problems - add `package-lock.json` - upgrade ancient `sinon` - add `should-sinon` to bridge `sinon` and `should` - add docs - made the helper object a class - replace some `var`s with `const`s
1 parent 508b795 commit c938fde

File tree

4 files changed

+439
-191
lines changed

4 files changed

+439
-191
lines changed

README.md

Lines changed: 143 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,13 @@ helper.init(require.resolve('node-red'));
6060

6161
describe('lower-case Node', function () {
6262

63-
afterEach(function () {
64-
helper.unload();
63+
beforeEach(function (done) {
64+
helper.startServer(done);
65+
});
66+
67+
afterEach(function (done) {
68+
helper.unload();
69+
helper.stopServer(done);
6570
});
6671

6772
it('should be loaded', function (done) {
@@ -109,6 +114,142 @@ The second test uses a `helper` node in the runtime connected to the output of o
109114

110115
To send a message into the `lower-case` node `n1` under test we call `n1.receive({ payload: "UpperCase" })` on that node. We can then check that the payload is indeed lower case in the `helper` node input event handler.
111116

117+
## Working with Spies
118+
119+
A Spy ([docs](http://sinonjs.org/releases/v5.0.6/spies/)) helps you collect information about how many times a function was called, with what, what it returned, etc.
120+
121+
This helper library automatically creates spies for the following functions on `Node.prototype` (these are the same functions as mentioned in the ["Creating Nodes" guide](https://nodered.org/docs/creating-nodes/node-js)):
122+
123+
- `trace()`
124+
- `debug()`
125+
- `warn()`
126+
- `log()`
127+
- `status()`
128+
- `send()`
129+
130+
> **Warning:** Don't try to spy on these functions yourself with `sinon.spy()`; since they are already spies, Sinon will throw an exception!
131+
132+
### Synchronous Example: Initialization
133+
134+
The `FooNode` `Node` will call `warn()` when it's initialized/constructed if `somethingGood` isn't present in the config, like so:
135+
136+
```js
137+
// /path/to/foo-node.js
138+
module.exports = function FooNode (config) {
139+
RED.nodes.createNode(this, config);
140+
141+
if (!config.somethingGood) {
142+
this.warn('badness');
143+
}
144+
}
145+
```
146+
147+
You can then assert:
148+
149+
```js
150+
// /path/to/test/foo-node_spec.js
151+
const FooNode = require('/path/to/foo-node');
152+
153+
it('should warn if the `somethingGood` prop is falsy', function (done) {
154+
const flow = {
155+
name: 'n1',
156+
somethingGood: false,
157+
/* ..etc.. */
158+
};
159+
helper.load(FooNode, flow, function () {
160+
n1.warn.should.be.calledWithExactly('badness');
161+
done();
162+
});
163+
});
164+
```
165+
166+
### Synchronous Example: Input
167+
168+
When it receives input, `FooNode` will immediately call `error()` if `msg.omg` is `true`:
169+
170+
```js
171+
// somewhere in FooNode constructor
172+
this.on('input', msg => {
173+
if (msg.omg) {
174+
this.error('lolwtf');
175+
}
176+
// ..etc..
177+
});
178+
```
179+
180+
Here's an example of how to make that assertion:
181+
182+
```js
183+
describe('if `omg` in input message', function () {
184+
it('should call `error` with "lolwtf" ', function (done) {
185+
const flow = {
186+
name: 'n1',
187+
/* ..etc.. */
188+
};
189+
helper.load(FooNode, flow, function () {
190+
const n1 = helper.getNode('n1')
191+
n1.receive({omg: true});
192+
n1.on('input', () => {
193+
n1.warn.should.be.calledWithExactly('lolwtf');
194+
done();
195+
});
196+
});
197+
});
198+
});
199+
```
200+
201+
### Asynchronous Example
202+
203+
Later in `FooNode`'s `input` listener, `warn()` may *asynchronously* be called, like so:
204+
205+
```js
206+
// somewhere in FooNode constructor function
207+
this.on('input', msg => {
208+
if (msg.omg) {
209+
this.error('lolwtf');
210+
}
211+
// ..etc..
212+
213+
Promise.resolve()
214+
.then(() => {
215+
if (msg.somethingBadAndWeird) {
216+
this.warn('bad weirdness');
217+
}
218+
});
219+
});
220+
```
221+
222+
The strategy in the previous example used for testing behavior of `msg.omg` will *not* work! `n1.warn.should.be.calledWithExactly('bad weirdness')` will throw an `AssertionError`, because `warn()` hasn't been called yet; `EventEmitter`s are synchronous, and the test's `input` listener is called directly after the `input` listener in `FooNode`'s function finished--but *before* the `Promise` is resolved!
223+
224+
Since we don't know *when* exactly `warn()` will get called (short of the slow, race-condition-prone solution of using a `setTimeout` and waiting *n* milliseconds, *then* checking), we need a different way to inspect the call. Miraculously, this helper module provides a solution.
225+
226+
The helper will cause the `FooNode` to asynchronously emit an event when `warn` is called (as well as the other methods in the above list). The event name will be of the format `call:<methodName>`; in this case, `methodName` is `warn`, so the event name is `call:warn`. The event Will pass a single argument: a Spy Call object ([docs](http://sinonjs.org/releases/v5.0.6/spy-call/)) corresponding to the latest method call. You can then make an assertion against this Spy Call argument, like so:
227+
228+
```js
229+
describe('if `somethingBadAndWeird` in input msg', function () {
230+
it('should call "warn" with "bad weirdness" ', function (done) {
231+
const flow = {
232+
name: 'n1',
233+
/* ..etc.. */
234+
};
235+
helper.load(FooNode, flow, function () {
236+
const n1 = helper.getNode('n1')
237+
n1.receive({somethingBadAndWeird: true});
238+
// because the emit happens asynchronously, this listener
239+
// will be registered before `call:warn` is emitted.
240+
n1.on('call:warn', call => {
241+
call.should.be.calledWithExactly('bad weirdness');
242+
done();
243+
});
244+
});
245+
});
246+
});
247+
```
248+
249+
As you can see, looks very similar to the synchronous solution; the only differences are the event name and assertion target.
250+
251+
> **Note**: The "asynchronous" strategy will also work *if and only if* a synchronous call to the spy is *still the most recent* when we attempt to make the assertion. This can lead to subtle bugs when refactoring, so exercise care when choosing which strategy to use.
252+
112253
## Running your tests
113254

114255
To run your tests:

0 commit comments

Comments
 (0)