Skip to content

Testing

There are three levels, from fastest to most realistic.

1. Unit-test the pure state machine

CircuitBreakerState is pure and time-injected — pass explicit now values, no Redis, no fake timers:

typescript
import { describe, it, expect } from 'vitest';
import { CircuitBreakerState } from '@nestjs-redisx/circuit-breaker';

describe('breaker policy', () => {
  it('trips after the threshold and recovers after cooldown', () => {
    const cb = new CircuitBreakerState({
      failureThreshold: 2,
      windowMs: 1000,
      openDurationMs: 5000,
      halfOpenMaxCalls: 1,
      successThreshold: 1,
    });

    cb.recordFailure(100);
    cb.recordFailure(200);
    expect(cb.snapshot(200).state).toBe('open');

    // still open before the cooldown, half-open after
    expect(cb.canRequest(300)).toBe(false);
    expect(cb.canRequest(200 + 5000)).toBe(true);

    cb.recordSuccess(200 + 5000);
    expect(cb.snapshot(200 + 5000).state).toBe('closed');
  });
});

2. Mock CircuitBreakerService

When unit-testing a service that injects CIRCUIT_BREAKER_SERVICE, provide a stub:

typescript
import { Test } from '@nestjs/testing';
import { CIRCUIT_BREAKER_SERVICE, type ICircuitBreakerService } from '@nestjs-redisx/circuit-breaker';
import { describe, it, expect, vi, type MockedObject } from 'vitest';

describe('UsersService', () => {
  it('runs the guarded function', async () => {
    const breaker: Partial<ICircuitBreakerService> = {
      // pass calls straight through in unit tests
      execute: vi.fn(async (_key, fn) => fn()),
      getState: vi.fn(),
      reset: vi.fn(),
    };

    const module = await Test.createTestingModule({
      providers: [UsersService, { provide: CIRCUIT_BREAKER_SERVICE, useValue: breaker }],
    }).compile();

    const service = module.get(UsersService);
    await service.getUser('42');
    expect(breaker.execute).toHaveBeenCalled();
  });
});

3. Run the real plugin on the in-memory driver (no Redis)

@nestjs-redisx/testing provides an in-memory Redis driver that runs the real plugin — including the Lua scripts — so you can exercise the full closed → open → half-open → closed cycle without a Redis server:

typescript
import { Test } from '@nestjs/testing';
import { RedisModule } from '@nestjs-redisx/core';
import { MEMORY_DRIVER_TYPE } from '@nestjs-redisx/testing';
import { CircuitBreakerPlugin, CIRCUIT_BREAKER_SERVICE, CircuitBreakerOpenError, type ICircuitBreakerService } from '@nestjs-redisx/circuit-breaker';
import { describe, it, expect } from 'vitest';

describe('breaker on the memory driver', () => {
  it('opens after failures and rejects fast', async () => {
    const app = await Test.createTestingModule({
      imports: [
        RedisModule.forRoot({
          clients: { type: 'single', host: 'x', port: 1 },
          global: { driver: MEMORY_DRIVER_TYPE },
          plugins: [new CircuitBreakerPlugin({ failureThreshold: 2, openDurationMs: 1000 })],
        }),
      ],
    }).compile();
    await app.init();
    const cb = app.get<ICircuitBreakerService>(CIRCUIT_BREAKER_SERVICE);

    const fail = () => Promise.reject(new Error('down'));
    await expect(cb.execute('dep', fail)).rejects.toThrow();
    await expect(cb.execute('dep', fail)).rejects.toThrow();

    expect((await cb.getState('dep')).state).toBe('open');
    await expect(cb.execute('dep', () => Promise.resolve('x'))).rejects.toBeInstanceOf(CircuitBreakerOpenError);

    await app.close();
  });
});

Time-dependent transitions

The distributed store reads time via Date.now(), so tests that cross the OPEN cooldown either wait for a small openDurationMs or assert the pre-cooldown behaviour. The pure CircuitBreakerState (level 1) needs no waiting — pass the now you want.

Released under the MIT License.