Skip to content

Commit 3d96d69

Browse files
committed
[New] add t.capture and t.captureFn, modeled after tap
1 parent c45db4e commit 3d96d69

File tree

5 files changed

+307
-1
lines changed

5 files changed

+307
-1
lines changed

lib/test.js

+74
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,18 @@ var EventEmitter = require('events').EventEmitter;
88
var has = require('has');
99
var isRegExp = require('is-regex');
1010
var trim = require('string.prototype.trim');
11+
var callBind = require('call-bind');
1112
var callBound = require('call-bind/callBound');
1213
var forEach = require('for-each');
1314
var inspect = require('object-inspect');
15+
var mockProperty = require('mock-property');
16+
1417
var isEnumerable = callBound('Object.prototype.propertyIsEnumerable');
1518
var toLowerCase = callBound('String.prototype.toLowerCase');
1619
var $exec = callBound('RegExp.prototype.exec');
1720
var objectToString = callBound('Object.prototype.toString');
21+
var $push = callBound('Array.prototype.push');
22+
var $slice = callBound('Array.prototype.slice');
1823

1924
var nextTick = typeof setImmediate !== 'undefined'
2025
? setImmediate
@@ -171,6 +176,75 @@ Test.prototype.teardown = function (fn) {
171176
}
172177
};
173178

179+
function wrapFunction(original) {
180+
if (typeof original !== 'undefined' && typeof original !== 'function') {
181+
throw new TypeError('`original` must be a function or `undefined`');
182+
}
183+
184+
var bound = original && callBind.apply(original);
185+
186+
var calls = [];
187+
188+
var wrapObject = {
189+
__proto__: null,
190+
wrapped: function wrapped() {
191+
var args = $slice(arguments);
192+
var completed = false;
193+
try {
194+
var returned = original ? bound(this, arguments) : void undefined;
195+
$push(calls, { args: args, receiver: this, returned: returned });
196+
completed = true;
197+
return returned;
198+
} finally {
199+
if (!completed) {
200+
$push(calls, { args: args, receiver: this, threw: true });
201+
}
202+
}
203+
},
204+
calls: calls,
205+
results: function results() {
206+
try {
207+
return calls;
208+
} finally {
209+
calls = [];
210+
wrapObject.calls = calls;
211+
}
212+
}
213+
};
214+
return wrapObject;
215+
}
216+
217+
Test.prototype.capture = function capture(obj, method) {
218+
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) {
219+
throw new TypeError('`obj` must be an object');
220+
}
221+
if (typeof method !== 'string' && typeof method !== 'symbol') {
222+
throw new TypeError('`method` must be a string or a symbol');
223+
}
224+
var implementation = arguments.length > 2 ? arguments[2] : void undefined;
225+
if (typeof implementation !== 'undefined' && typeof implementation !== 'function') {
226+
throw new TypeError('`implementation`, if provided, must be a function');
227+
}
228+
229+
var wrapper = wrapFunction(implementation);
230+
var restore = mockProperty(obj, method, { value: wrapper.wrapped });
231+
this.teardown(restore);
232+
233+
wrapper.results.restore = restore;
234+
235+
return wrapper.results;
236+
};
237+
238+
Test.prototype.captureFn = function captureFn(original) {
239+
if (typeof original !== 'function') {
240+
throw new TypeError('`original` must be a function');
241+
}
242+
243+
var wrapObject = wrapFunction(original);
244+
wrapObject.wrapped.calls = wrapObject.calls;
245+
return wrapObject.wrapped;
246+
};
247+
174248
Test.prototype._end = function (err) {
175249
var self = this;
176250
if (this._progeny.length) {

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"inherits": "~2.0.4",
2525
"is-regex": "~1.1.4",
2626
"minimist": "~1.2.8",
27+
"mock-property": "~1.0.0",
2728
"object-inspect": "~1.12.3",
2829
"resolve": "~1.22.6",
2930
"string.prototype.trim": "~1.2.8"

readme.markdown

+25-1
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,8 @@ You may pass the same options that [`test()`](#testname-opts-cb) accepts.
360360

361361
## t.comment(message)
362362

363-
Print a message without breaking the tap output. (Useful when using e.g. `tap-colorize` where output is buffered & `console.log` will print in incorrect order vis-a-vis tap output.)
363+
Print a message without breaking the tap output.
364+
(Useful when using e.g. `tap-colorize` where output is buffered & `console.log` will print in incorrect order vis-a-vis tap output.)
364365

365366
Multiline output will be split by `\n` characters, and each one printed as a comment.
366367

@@ -372,6 +373,29 @@ Assert that `string` matches the RegExp `regexp`. Will fail when the first two a
372373

373374
Assert that `string` does not match the RegExp `regexp`. Will fail when the first two arguments are the wrong type.
374375

376+
## t.capture(obj, method, implementation = () => {})
377+
378+
Replaces `obj[method]` with the supplied implementation.
379+
`obj` must be a non-primitive, `method` must be a valid property key (string or symbol), and `implementation`, if provided, must be a function.
380+
381+
Calling the returned `results()` function will return an array of call result objects.
382+
The array of calls will be reset whenever the function is called.
383+
Call result objects will match one of these forms:
384+
- `{ args: [x, y, z], receiver: o, returned: a }`
385+
- `{ args: [x, y, z], receiver: o, threw: true, thrown: e }`
386+
387+
The replacement will automatically be restored on test teardown.
388+
You can restore it manually, if desired, by calling `.restore()` on the returned results function.
389+
390+
Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html).
391+
392+
## t.captureFn(original)
393+
394+
Wraps the supplied function.
395+
The returned wrapper has a `.calls` property, which is an array that will be populated with call result objects, described under `t.capture()`.
396+
397+
Modeled after [tap](https://tapjs.github.io/tapjs/modules/_tapjs_intercept.html).
398+
375399
## var htest = test.createHarness()
376400

377401
Create a new test harness instance, which is a function like `test()`, but with a new pending stack and test state.

test/capture.js

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
'use strict';
2+
3+
var tape = require('../');
4+
var tap = require('tap');
5+
var concat = require('concat-stream');
6+
var inspect = require('object-inspect');
7+
var forEach = require('for-each');
8+
var v = require('es-value-fixtures');
9+
10+
var stripFullStack = require('./common').stripFullStack;
11+
12+
tap.test('capture: output', function (tt) {
13+
tt.plan(1);
14+
15+
var test = tape.createHarness();
16+
var count = 0;
17+
test.createStream().pipe(concat(function (body) {
18+
tt.same(stripFullStack(body.toString('utf8')), [].concat(
19+
'TAP version 13',
20+
'# argument validation',
21+
v.primitives.map(function (x) {
22+
return 'ok ' + ++count + ' ' + inspect(x) + ' is not an Object';
23+
}),
24+
v.nonPropertyKeys.map(function (x) {
25+
return 'ok ' + ++count + ' ' + inspect(x) + ' is not a valid property key';
26+
}),
27+
v.nonFunctions.filter(function (x) { return typeof x !== 'undefined'; }).map(function (x) {
28+
return 'ok ' + ++count + ' ' + inspect(x) + ' is not a function';
29+
}),
30+
'# captures calls',
31+
'ok ' + ++count + ' property has expected initial value',
32+
'# capturing',
33+
'ok ' + ++count + ' throwing implementation throws',
34+
'ok ' + ++count + ' should be equivalent',
35+
'ok ' + ++count + ' should be equivalent',
36+
'ok ' + ++count + ' should be equivalent',
37+
'ok ' + ++count + ' should be equivalent',
38+
'ok ' + ++count + ' should be equivalent',
39+
'# post-capturing',
40+
'ok ' + ++count + ' property is restored',
41+
'ok ' + ++count + ' added property is removed',
42+
'',
43+
'1..' + count,
44+
'# tests ' + count,
45+
'# pass ' + count,
46+
'',
47+
'# ok',
48+
''
49+
));
50+
}));
51+
52+
test('argument validation', function (t) {
53+
forEach(v.primitives, function (primitive) {
54+
t.throws(
55+
function () { t.capture(primitive, ''); },
56+
TypeError,
57+
inspect(primitive) + ' is not an Object'
58+
);
59+
});
60+
61+
forEach(v.nonPropertyKeys, function (nonPropertyKey) {
62+
t.throws(
63+
function () { t.capture({}, nonPropertyKey); },
64+
TypeError,
65+
inspect(nonPropertyKey) + ' is not a valid property key'
66+
);
67+
});
68+
69+
forEach(v.nonFunctions, function (nonFunction) {
70+
if (typeof nonFunction !== 'undefined') {
71+
t.throws(
72+
function () { t.capture({}, '', nonFunction); },
73+
TypeError,
74+
inspect(nonFunction) + ' is not a function'
75+
);
76+
}
77+
});
78+
79+
t.end();
80+
});
81+
82+
test('captures calls', function (t) {
83+
var sentinel = { sentinel: true, inspect: function () { return '{ SENTINEL OBJECT }'; } };
84+
var o = { foo: sentinel, inspect: function () { return '{ o OBJECT }'; } };
85+
t.equal(o.foo, sentinel, 'property has expected initial value');
86+
87+
t.test('capturing', function (st) {
88+
var results = st.capture(o, 'foo', function () { return sentinel; });
89+
var results2 = st.capture(o, 'foo2');
90+
var up = new SyntaxError('foo');
91+
var resultsThrow = st.capture(o, 'fooThrow', function () { throw up; });
92+
93+
o.foo(1, 2, 3);
94+
o.foo(3, 4, 5);
95+
o.foo2.call(sentinel, 1);
96+
st.throws(
97+
function () { o.fooThrow(1, 2, 3); },
98+
SyntaxError,
99+
'throwing implementation throws'
100+
);
101+
102+
st.deepEqual(results(), [
103+
{ args: [1, 2, 3], receiver: o, returned: sentinel },
104+
{ args: [3, 4, 5], receiver: o, returned: sentinel }
105+
]);
106+
st.deepEqual(results(), []);
107+
108+
o.foo(6, 7, 8);
109+
st.deepEqual(results(), [
110+
{ args: [6, 7, 8], receiver: o, returned: sentinel }
111+
]);
112+
113+
st.deepEqual(results2(), [
114+
{ args: [1], receiver: sentinel, returned: undefined }
115+
]);
116+
st.deepEqual(resultsThrow(), [
117+
{ args: [1, 2, 3], receiver: o, threw: true }
118+
]);
119+
120+
st.end();
121+
});
122+
123+
t.test('post-capturing', function (st) {
124+
st.equal(o.foo, sentinel, 'property is restored');
125+
st.notOk('foo2' in o, 'added property is removed');
126+
127+
st.end();
128+
});
129+
130+
t.end();
131+
});
132+
});

test/captureFn.js

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
'use strict';
2+
3+
var tape = require('../');
4+
var tap = require('tap');
5+
var concat = require('concat-stream');
6+
var inspect = require('object-inspect');
7+
var forEach = require('for-each');
8+
var v = require('es-value-fixtures');
9+
10+
var stripFullStack = require('./common').stripFullStack;
11+
12+
tap.test('captureFn: output', function (tt) {
13+
tt.plan(1);
14+
15+
var test = tape.createHarness();
16+
var count = 0;
17+
test.createStream().pipe(concat(function (body) {
18+
tt.same(stripFullStack(body.toString('utf8')), [].concat(
19+
'TAP version 13',
20+
'# argument validation',
21+
v.nonFunctions.map(function (x) {
22+
return 'ok ' + ++count + ' ' + inspect(x) + ' is not a function';
23+
}),
24+
'# captured fn calls',
25+
'ok ' + ++count + ' return value is passed through',
26+
'ok ' + ++count + ' throwing implementation throws',
27+
'ok ' + ++count + ' should be equivalent',
28+
'ok ' + ++count + ' should be equivalent',
29+
'',
30+
'1..' + count,
31+
'# tests ' + count,
32+
'# pass ' + count,
33+
'',
34+
'# ok',
35+
''
36+
));
37+
}));
38+
39+
test('argument validation', function (t) {
40+
forEach(v.nonFunctions, function (nonFunction) {
41+
t.throws(
42+
function () { t.captureFn(nonFunction); },
43+
TypeError,
44+
inspect(nonFunction) + ' is not a function'
45+
);
46+
});
47+
48+
t.end();
49+
});
50+
51+
test('captured fn calls', function (t) {
52+
var sentinel = { sentinel: true, inspect: function () { return '{ SENTINEL OBJECT }'; } };
53+
54+
var wrappedSentinelThunk = t.captureFn(function () { return sentinel; });
55+
var up = new SyntaxError('foo');
56+
var wrappedThrower = t.captureFn(function () { throw up; });
57+
58+
t.equal(wrappedSentinelThunk(1, 2), sentinel, 'return value is passed through');
59+
t.throws(
60+
function () { wrappedThrower.call(sentinel, 1, 2, 3); },
61+
SyntaxError,
62+
'throwing implementation throws'
63+
);
64+
65+
t.deepEqual(wrappedSentinelThunk.calls, [
66+
{ args: [1, 2], receiver: undefined, returned: sentinel }
67+
]);
68+
69+
t.deepEqual(wrappedThrower.calls, [
70+
{ args: [1, 2, 3], receiver: sentinel, threw: true }
71+
]);
72+
73+
t.end();
74+
});
75+
});

0 commit comments

Comments
 (0)