Why tests can be flaky
slowdep's lognormal sampling calls Math.random() on every invocation. The sampled delay is different on every call. This is the point — it produces realistic variance. But it creates a problem for tests: if your test measures elapsed time or depends on the timing of events, it will produce different results on different runs.
A test that asserts "the retry was attempted within 300ms" might pass when the sampled delay was 4ms and fail when it was 190ms. A test with a tight setTimeout-based assertion might race depending on the random sample. These are real flaky tests that slow down CI and erode trust.
The solution depends on what you're testing. Three approaches are described below — from most surgical (mock Math.random) to most complete (bypass slowdep entirely in tests).
Mock Math.random
The most targeted approach: intercept Math.random and return a fixed value. Since slowdep uses Math.random() for the Box-Muller transform, a fixed random value produces a fixed latency on every call.
A value of 0.5 produces latency close to the p50. A value close to 0 or 1 produces extreme samples — useful for testing timeout paths.
import { withLatency } from 'slowdep' describe('UserService', () => { let randomSpy: jest.SpyInstance beforeEach(() => { // Lock Math.random — lognormal sampling is now deterministic randomSpy = jest.spyOn(Math, 'random').mockReturnValue(0.5) }) afterEach(() => { // Always restore — don't let the mock leak to other tests randomSpy.mockRestore() }) it('returns user on success', async () => { const mockQuery = jest.fn().mockResolvedValue({ id: 1, name: 'Alice' }) const slowQuery = withLatency(mockQuery, 'postgres') const user = await slowQuery(1) // Latency is now ~p50 every time — test is deterministic expect(user.name).toBe('Alice') }) it('triggers timeout on slow response', async () => { // Use a value close to 0 to sample near p99 randomSpy.mockReturnValue(0.01) const slowQuery = withLatency(mockQuery, 'postgres') // This test exercises the slow path reliably await expect(withTimeout(slowQuery(1), 10)).rejects.toThrow('timeout') }) })
Mock the whole delay
If you don't care about measuring latency at all — you just want your unit tests to run instantly — mock withLatency to return the original function unchanged. This removes all timing from the equation.
// In your test setup file or at the top of the test: jest.mock('slowdep', () => ({ withLatency: (fn: any) => fn, withLatencyAll: (client: any) => client, }))
This approach is best for unit tests that focus on business logic rather than resilience behavior. If you're testing that your retry code works, you probably want slowdep active (or Approach 1). If you're testing that createOrder calls insertOrder with the right arguments, mocking the whole thing is cleaner.
With Vitest, use vi.mock instead:
vi.mock('slowdep', () => ({ withLatency: (fn: any) => fn, withLatencyAll: (client: any) => client, }))
Environment variable bypass
A user-implemented pattern that makes the bypass explicit and observable: wrap the withLatency call in a conditional that checks an environment variable. In your test environment, set SLOWDEP_BYPASS=1 and slowdep is never called at all.
SLOWDEP_BYPASS is not a built-in flag. This is a pattern you implement in your own code. slowdep itself always applies latency when called — the bypass lives in your wrapper.
// lib/slow.ts — your wrapper module import { withLatency } from 'slowdep' import type { LatencyProfile } from 'slowdep' export function maybeSlowDep< T extends (...args: any[]) => Promise<any> >(fn: T, profile: LatencyProfile): T { if (process.env.SLOWDEP_BYPASS === '1') return fn if (process.env.NODE_ENV === 'production') return fn return withLatency(fn, profile) }
// Usage in your service code import { maybeSlowDep } from '../lib/slow' const slowQuery = maybeSlowDep(queryUser, 'postgres') // In test environment with SLOWDEP_BYPASS=1: // slowQuery === queryUser (no latency) // In development without the env var: // slowQuery is the wrapped version with postgres latency
Set the variable in your test runner config:
// jest.config.js export default { testEnvironment: 'node', testEnvironmentOptions: { env: { SLOWDEP_BYPASS: '1' } } }
What you should and shouldn't assert
The goal of slowdep is to help you test resilience behavior — not to let you assert exact millisecond values. Here's a practical guide:
That the call took exactly N milliseconds. Latency is intentionally variable and any assertion on exact timing is fragile. Don't assert elapsed < 10 or elapsed === 5.
That your code handles both fast and slow responses correctly. Test that retries fire when the error rate triggers. Test that your timeout logic cancels the operation when latency exceeds the deadline. Test that your circuit breaker trips after N failures. These behaviors are what slowdep exists to surface.
// Good: testing resilience behavior, not timing it('retries once on transient error then succeeds', async () => { const mockFn = jest.fn() .mockRejectedValueOnce(new Error('Simulated transient error')) .mockResolvedValueOnce({ id: 1 }) const result = await withRetry(withLatency(mockFn, 'postgres')) expect(mockFn).toHaveBeenCalledTimes(2) expect(result.id).toBe(1) })