-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Closed
Labels
Description
I wanted to quickly write this up because I think that AVA could provide a superior/simpler developer experience for stubbing within tests than tools like Sinon.
Instead of writing a detailed proposal, I'm going to give some API examples and we can refine them from there. I will also create a library that implements as much of this as possible.
Why add stubs/spies
- Because it's a very common need for testing larger apps
- Because existing tools are very complex and have massive API surface
- Because AVA can provide a better experience by integrating them into the testing framework
- Through better error messages
- Through automatic cleanup
- Through smaller APIs
Why not a separate AVA-specific library
- Because error messages could be greatly improved in AVA by recognizing assertions on stub/spy calls
Real World Examples
const test = require('ava');
const EventEmitter = require('events');
test('EventEmitter', t => {
let e = new EventEmitter();
let s = t.stub();
e.on('event', s);
e.emit('event');
t.is(s.calls.length, 1);
e.emit('event', 'arg');
t.is(s.calls[1].arguments[0], 'arg');
});const test = require('ava');
const api = require('./api');
test('api.getCurrentUser()', t => {
let s = t.spy(api, 'request', () => {
return Promise.resolve({ id: 42 });
});
await api.getCurrentUser();
t.deepEqual(s.calls[0].args[0], {
method: 'GET',
url: '/api/v1/user',
});
});Detailed API Examples
test('t.stub()', t => {
// create stub with optional inner function which can be used
// to customize behavior. Otherwise will default to: `() => {}`
let s = t.stub(arg => {
// use s.calls inside optional inner func instead of complex
// `s.onCall(0).assertArgs('s1').returns('r1');`
if (s.calls.length === 0) { t.is(arg, 'a1'); return 'r1'; }
if (s.calls.length === 1) { t.is(arg, 'a2'); return 'r2'; }
if (s.calls.length === 2) { t.is(arg, 'a3'); return 'r3'; }
throw new Error('too many calls');
});
// use as regular function
s.call('t1', 'a1');
s.call('t2', 'a2');
s.call('t3', 'a3');
// assert using existing AVA methods, AVA's error reporting is
// already better than the custom stub assertion methods
// different testing libs create.
// AVA could also improve error messages by detecting that
// we're writing assertions against a stub/spy.
t.deepEqual(s.calls, [
{ this: 't1', arguments: ['a1'], return: 'r1' },
{ this: 't2', arguments: ['a2'], return: 'r2' },
{ this: 't3', arguments: ['a3'], return: 'r3' },
]);
t.throws(() => {
s.call('t4', 'a4'); // Error: too many calls
});
});
test('t.spy()', t => {
// create spy with optional inner function which can be used
// to customize behavior and override the original function.
// Otherwise will default to calling the original function.
// object['method'] must be a defined function or t.spy() will
// throw
let s = t.spy(object, 'method', (...args) => {
// use s.calls to customize this function the same way you
// would with t.stub()
// use s.original to access the original function if you want to
// call it.
return s.original(...args);
});
// use as regular function and write assertions the same way you
// do with t.stub()
// If you want to restore the original function you can simply do:
object.method = s.original;
// otherwise AVA will automatically restore the original function
// at the end of a test run.
});API type signature
// generic function (`this` refers to function's call context `this`, not a first arg)
type Func<T, A, R> = (this: T, ...arguments: A): R;
// t.spy/stub().calls array
type Calls<T, A, R> = Array<{
this: T,
arguments: A,
returns: R,
}>;
// callable functions with properties
type Stub<T, A, R> = Func<T, A, R> & { calls: Calls<T, A, R> };
type Spy<F, T, A, R> = Func<T, A, R> & { calls: Calls<T, A, R>, original: F };
// test('...', t => { t.stub/spy() });
interface t {
stub<T, A, R>(inner?: Func<T, A, R>): Stub<T, A, R>;
spy<F, T, A, R>(inner?: Func<T, A, R>): Spy<F, T, A, R>;
}skellock, jacobwgillespie, OmgImAlexis, bensleveritt, g-ongenae and 20 more