The problem

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).

Approach 1

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')
  })
})
Approach 2

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,
}))
When to use this

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,
}))
Approach 3

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.

User-implemented pattern

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 to test

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:

Don't test

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.

Do test

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)
})