Skip to content

How to inspect calls with spy()

Use spy() to read the call history of a proxy object and assert which functions were called, with which arguments, and in what order.


Import spy

spy is exported from 'utest':

import { describe, it, assert, mock, spy, contains, any } from 'utest';

Read call records from a proxy

Pass any proxy returned by mock.inject() or mock.global.patch() to spy(). It returns an object with a calls map — one key per method, each holding an array of argument arrays:

describe('call recording', () => {
    it('records arguments in order', () => {
        mock.inject('fs', { data: { '/a': 'x', '/b': 'y' } }, (m_fs) => {
            m_fs.readfile('/a');
            m_fs.readfile('/b');
            assert.match([ ['/a'], ['/b'] ], spy(m_fs).calls.readfile);
        });
    });
});

Verify that a method was not called

Check that the array for the method is empty:

mock.inject('fs', {}, (m_fs) => {
    // no calls made
    assert.match([], spy(m_fs).calls.readfile);
});

Verify multiple methods independently

Each method has its own entry in calls. Use contains to check only the methods you care about without specifying every method:

mock.inject('fs', { data: {} }, (m_fs) => {
    m_fs.writefile('/out', 'hello');
    m_fs.access('/out', 'r');
    assert.match(contains({
        writefile: [ ['/out', 'hello'] ],
        access:    [ ['/out', 'r'] ]
    }), spy(m_fs).calls);
});

Use any() to ignore specific arguments

When you care that a function was called but not what the exact arguments were:

mock.inject('uloop', {}, (m_uloop) => {
    m_uloop.timer(100, () => {});
    m_uloop.timer(200, () => {});
    // verify timers were registered — ignore callback functions
    assert.match([
        [ 100, any() ],
        [ 200, any() ]
    ], spy(m_uloop).calls.timer);
});

Spy on nested objects (cursors, connections)

For proxies that return inner objects (uci.cursor(), ubus.connect(), uclient.new()), call spy() on the inner object:

mock.inject('uci', {
    data: { network: { wan: { '.type': 'interface', proto: 'dhcp' } } }
}, (m_uci) => {
    let cursor = m_uci.cursor();
    cursor.get('network', 'wan', 'proto');
    cursor.set('network', 'wan', 'proto', 'static');
    assert.match(contains({
        get: [ ['network', 'wan', 'proto'] ],
        set: [ ['network', 'wan', 'proto', 'static'] ]
    }), spy(cursor).calls);
});

Each cursor() call returns an independent object with its own call log:

mock.inject('uci', { data: {} }, (m_uci) => {
    let c1 = m_uci.cursor();
    let c2 = m_uci.cursor();
    c1.get('net', 'sec', 'opt');
    assert.match([ ['net', 'sec', 'opt'] ], spy(c1).calls.get);
    assert.match([], spy(c2).calls.get);
});

Verify calls made through global.patch()

spy() works on proxies from mock.global.patch() just as with mock.inject():

// import at top of file
import * as fs from 'fs';

const m_fs = mock.global.patch('fs', { data: { '/g': 'global' } });
fs.readfile('/g');
assert.match([ ['/g'] ], spy(m_fs).calls.readfile);
mock.global.unpatch('fs');

Call records reset between inject() scopes

A new mock.inject() call starts with empty call records — it does not inherit calls from a previous inject on the same module:

mock.inject('fs', { data: { '/a': 'x' } }, (m_fs) => {
    m_fs.readfile('/a');
    assert.match([ ['/a'] ], spy(m_fs).calls.readfile);
});

mock.inject('fs', { data: { '/a': 'x' } }, (m_fs) => {
    // fresh scope — no calls yet
    assert.match([], spy(m_fs).calls.readfile);
});

Next steps