Thanks to visit codestin.com
Credit goes to github.com

Skip to content

RFC: Stubs & Spies #1825

@jamiebuilds

Description

@jamiebuilds

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>;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions