Writing your first mock¶
In this tutorial, we will write a source module that accepts fs as a parameter, and write tests that pass a mock proxy in place of the real module. By the end you will understand how mock.inject() works and why dependency injection makes mocking simpler.
What we will build¶
A source module src/banner.uc that accepts fs as a parameter, and a test file test/unit/banner_test.uc that uses mock.inject() to supply a virtual filesystem. No config file is needed.
Prerequisites¶
- Completed Writing your first test suite. You should be comfortable with
describe,it,assert, and the-lflag.
Step 1 — Create the source module¶
Write src/banner.uc:
export function get_banner(fs) {
return fs.readfile('/etc/banner');
};
get_banner receives fs as a parameter instead of importing it directly. In production it is called with the real fs module; in tests we will pass a mock proxy.
Step 2 — Create the test file¶
Create test/unit/banner_test.uc:
import { describe, it, mock, assert } from 'utest';
import { get_banner } from 'banner';
describe("get_banner()", () => {
it("returns the seeded banner content", () => {
mock.inject('fs', {
data: { '/etc/banner': 'Welcome to OpenWrt\n' }
}, (m_fs) => {
assert.match('Welcome to OpenWrt\n', get_banner(m_fs));
});
});
it("returns null when the banner file is absent", () => {
mock.inject('fs', { data: { '/etc/banner': null } }, (m_fs) => {
assert.match(null, get_banner(m_fs));
});
});
});
mock.inject('fs', ...) builds a proxy and passes it as m_fs to the callback. We hand that proxy directly to get_banner, so the function reads from virtual data instead of the real filesystem. No utest.config.uc is needed.
Seeding a path to null marks it explicitly absent, so readfile returns null. This matters here: for a path you simply leave unseeded, a non-strict mock falls through to the real filesystem — and /etc/banner exists on a real OpenWrt system — so seeding null is how you assert "this file is not there."
Step 3 — Run the tests¶
utest -l src test/unit/banner_test.uc
You should see (tests run in a randomized order, so yours may differ):
[test/unit/banner_test.uc] test/unit/banner_test.uc (2 tests)
[PASS] returns the seeded banner content
[PASS] returns null when the banner file is absent
Summary:
Suites: 1
Total: 2
Passed: 2
Failed: 0
Errors: 0
Time: 5 ms
Seed: ...
Step 4 — Confirm the mock is bounded to the callback¶
The proxy is only valid inside its callback. Add a third test that saves the proxy and tries to use it after the callback has exited:
it("the proxy dies if used after the callback exits", () => {
let saved = null;
mock.inject('fs', { data: { '/etc/banner': 'test content' } }, (m_fs) => {
saved = m_fs;
assert.match('test content', get_banner(m_fs));
});
// The mock layer has been popped. Using the leaked proxy now dies loudly
// rather than silently falling through to the real filesystem.
assert.throws(() => get_banner(saved), /used outside its scope/);
});
Run again and confirm all three tests pass. The guard means a proxy can never quietly escape its scope and start returning real data where you expected mock data — a leaked proxy fails fast instead.
What we just built¶
- A source module that accepts
fsas a parameter so tests control which implementation it receives. - Tests that use
mock.inject()to build a proxy and pass it directly to the function under test. - Confirmation that the mock layer is bounded: once the callback exits, using the proxy dies instead of leaking real data into your test.
- No
utest.config.ucrequired when code receives its dependencies as parameters.
When your code imports fs directly¶
If your module uses import * as fs from 'fs' rather than accepting fs as a parameter, see How-to: Mock a module with mock.inject() for that pattern.
Next steps¶
- Mock the filesystem in detail (writefile, lsdir, glob, strict mode): How-to: Mock the filesystem
- Intercept calls through a direct
import * as fs from 'fs'binding: How-to: Patch global state - Understand why two mock mechanisms exist: About mock.inject() vs mock.global.patch()
- Make unmocked paths fail immediately: How-to: Use strict mode