You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
- 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
@@ -109,6 +114,142 @@ The second test uses a `helper` node in the runtime connected to the output of o
109
114
110
115
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.
111
116
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=functionFooNode (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
+
constFooNode=require('/path/to/foo-node');
152
+
153
+
it('should warn if the `somethingGood` prop is falsy', function (done) {
154
+
constflow= {
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
+
constflow= {
186
+
name:'n1',
187
+
/* ..etc.. */
188
+
};
189
+
helper.load(FooNode, flow, function () {
190
+
constn1=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
+
constflow= {
232
+
name:'n1',
233
+
/* ..etc.. */
234
+
};
235
+
helper.load(FooNode, flow, function () {
236
+
constn1=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.
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.
0 commit comments