Skip to content

How to mock a built-in global

Use mock.inject_builtin() to intercept built-in globals such as warn, system, and print. Built-ins are not loadable modules, so the shim-based mock API cannot reach them — inject_builtin works by writing directly to global[name] and guaranteeing cleanup even when the callback throws.


Capture warn() output for a single test

Use mock.inject_builtin() when the interception should be scoped to one test. Pass the name of the built-in, the replacement function, and a callback. The original is restored when the callback returns — or throws:

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

describe('my module', () => {
    it('emits a deprecation warning', () => {
        const captured = [];
        mock.inject_builtin('warn', (...args) => push(captured, join('', args)), () => {
            my_module.deprecated_function();
        });
        assert.match(contains('deprecated'), join('\n', captured));
    });
});

The original warn function is unconditionally restored when the callback exits. If an assertion inside the callback throws, the restore still happens before the exception propagates.


Suppress warn() output across multiple tests

inject_builtin works cleanly for this too — call it once per test and no cleanup step is needed. The callback wraps the test body; the original is restored when it returns:

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

describe('noisy module', () => {
    it('does not spam stderr', () => {
        mock.inject_builtin('warn', () => null, () => {
            noisy_module.run();
        });
    });

    it('still works when warn is suppressed', () => {
        mock.inject_builtin('warn', () => null, () => {
            assert.match('ok', noisy_module.result());
        });
    });
});

No afterEach, no risk of a leaked patch.


When to use patch_builtin instead

Reach for mock.global.patch_builtin() only when the replacement must be active outside a callback — for example, at file scope before any describe runs, or when the code under test is called from a module-level initialiser that executes before any test body:

// file-scope patch — active from here until explicitly removed
mock.global.patch_builtin('warn', () => null);

const fw4 = require('fw4');  // fw4's module-scope code runs here; warn is already suppressed

describe('fw4', () => {
    afterEach(() => { mock.global.unpatch_builtin('warn'); });
    // ...
});

Always pair patch_builtin with unpatch_builtin — in an afterEach if the patch is per-test, or explicitly at the end of the scope where it was installed. Forgetting to call unpatch_builtin leaks the replacement into subsequent tests.


Intercept system() calls

The same API works for any built-in global. To verify that production code calls system() with the right argument:

it('invokes nft with the generated ruleset path', () => {
    const calls = [];
    mock.inject_builtin('system', (cmd) => { push(calls, cmd); return 0; }, () => {
        fw4.apply_ruleset('/tmp/fw4.nft');
    });
    assert.match(1, length(calls));
    assert.match(contains('/tmp/fw4.nft'), calls[0]);
});

The replacement receives the same arguments as the real built-in and its return value is used by the caller. Return a sensible value — system() returns the exit code, so returning 0 signals success.


Layer inject_builtin on top of patch_builtin

If a persistent patch is already active and you need a tighter scope within one test, inject_builtin layers correctly on top of it:

describe('layering', () => {
    afterEach(() => { mock.global.unpatch_builtin('warn'); });

    it('uses a narrower replacement inside the callback', () => {
        const outer = [];
        const inner = [];
        mock.global.patch_builtin('warn', (...a) => push(outer, join('', a)));

        mock.inject_builtin('warn', (...a) => push(inner, join('', a)), () => {
            warn('inside');
        });

        warn('outside');
        // inner captured the call made inside the callback
        assert.match(['inside'], inner);
        // outer captured the call made after the callback returned
        assert.match(['outside'], outer);
    });
});

inject_builtin saves whatever is currently in global.warn at the time of the call — the patched function — and restores it when its callback exits. The persistent patch remains active after inject_builtin returns.


Next steps