Jasmine fake timers and Node.js DNS lookup callback timing
0 reputation · 08 Nov 2020, 16:18 UTC
0 reputation · 08 Nov 2020, 16:18 UTC
The goal is to write a unit test that exercises DNS‑resolution logic while using Jasmine’s fake‑timer utility to make all asynchronous callbacks fire synchronously, enabling deterministic, fast test execution.
However, Jasmine’s jasmine.clock() only replaces JavaScript‑based timers (setTimeout, setInterval, etc.). Node’s dns.lookup and tls.connect callbacks are performed via libuv’s thread pool, so they are not intercepted by the fake timer and still run on real OS threads, causing the test to wait for actual I/O or to produce false passes if the timer is advanced prematurely.
This raises an unresolved design question about whether fake timers should also mock libuv‑based I/O to achieve fully synchronous test runs.
Should Jasmine extend its fake‑timer implementation to intercept libuv thread‑pool callbacks? What would be the impact on test fidelity and existing suites that rely on real DNS/TLS behavior? How can developers currently obtain synchronous‑style tests without modifying Jasmine core?
Jasmine’s jasmine.clock() only patches JavaScript timers. It cannot intercept libuv‑based I/O such as dns.lookup or tls.connect. Extending the clock to fake those callbacks would break existing suites, hide real‑world latency and error paths, and introduce hard‑to‑debug regressions. The recommended approach is to stub or mock dns.lookup (or the whole dns module) with a synchronous implementation that immediately calls the callback, then use jasmine.clock().tick() to drive any timers in your code.
dns.lookupsinon or proxyquire) that can replace the dns module before your code under test is required.const sinon = require('sinon');
const dnsStub = sinon.stub();
dnsStub.withArgs('example.com', sinon.match.any, sinon.match.func).callsFake((host, opts, cb) => {
// Immediately invoke the callback with a fake address.
cb(null, '127.0.0.1', 4);
});
proxyquire to inject the stub:
const myModule = proxyquire('./myModule', { dns: { lookup: dnsStub } });
jasmine.clock().install(), execute the code, then jasmine.clock().tick() any timers you need to advance. The DNS callback will fire synchronously, keeping the test deterministic.dns.lookup after the test with dnsStub.restore().If your code path depends on DNS errors (e.g., handling ENOTFOUND or timeouts), you can configure the stub to call the callback with an error for specific hosts:
dnsStub.withArgs('bad-host', sinon.match.any, sinon.match.func).callsFake((host, opts, cb) => {
cb(new Error('ENOTFOUND')); // or cb({ code: 'ENOTFOUND' });
});
In such cases, the stub still runs synchronously, but you exercise the error handling logic.
Do you need to assert on actual DNS latency or error propagation in your tests? If so, a stub that mimics latency (e.g., using setTimeout) or an integration test with a real DNS server might be more appropriate.
Jasmine’s fake timers are intentionally limited to JavaScript timers. Extending them to cover libuv I/O would degrade test reliability and maintainability. The safest path is to stub dns.lookup (or the entire dns module) with a synchronous implementation, then use the fake timer to drive any timers in your code. This keeps tests fast, deterministic, and faithful to the real I/O behavior your code expects.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.