Skip to content

Testing Circuit Breakers — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Testing Circuit Breakers. We cover key concepts, practical examples, and best practices to help you master this topic.

Testing circuit breakers requires verifying state transitions under failure conditions, ensuring the circuit opens at the right threshold, closes on recovery, and integrates correctly with downstream services.

What You'll Learn

By the end of this tutorial, you will know how to write unit tests, integration tests, and chaos tests for circuit breaker implementations in Node.js and Python.

Why It Matters

Untested circuit breakers give false confidence. A misconfigured circuit that never opens or opens too late can cause the same cascading failures it was meant to prevent.

Real-World Use

DodaTech's CI pipeline includes circuit breaker tests that simulate database failures, network timeouts, and slow responses to verify every service handles degraded dependencies correctly.

Testing Circuit Breakers Learning Path

flowchart LR
  A[Circuit Breaker Implementation] --> B[Testing Circuit Breakers]
  B --> C[Unit Tests]
  B --> D[Integration Tests]
  B --> E[Chaos Tests]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Unit Testing State Transitions

The most important tests verify that the circuit transitions between states correctly under controlled conditions.

class TestableCircuitBreaker {
  constructor(threshold = 3, resetTimeout = 1000) {
    this.state = "closed";
    this.failureCount = 0;
    this.threshold = threshold;
    this.resetTimeout = resetTimeout;
    this.nextAttempt = Date.now();
  }

  async call(fn) {
    if (this.state === "open") {
      if (Date.now() > this.nextAttempt) {
        this.state = "half-open";
      } else {
        throw new Error("open");
      }
    }
    try {
      const result = await fn();
      this.failureCount = 0;
      this.state = "closed";
      return result;
    } catch (err) {
      this.failureCount++;
      if (this.failureCount >= this.threshold) {
        this.state = "open";
        this.nextAttempt = Date.now() + this.resetTimeout;
      }
      throw err;
    }
  }
}

async function testOpenOnThreshold() {
  const cb = new TestableCircuitBreaker(2, 10000);
  const fail = () => Promise.reject(new Error("fail"));

  await cb.call(fail).catch(() => {});
  console.log("State after 1 failure:", cb.state);

  await cb.call(fail).catch(() => {});
  console.log("State after 2 failures:", cb.state);
}

testOpenOnThreshold();
// State after 1 failure: closed
// State after 2 failures: open

Testing Half-Open Recovery

The half-open state allows one request through to test if the downstream service has recovered.

async function testHalfOpenCloses() {
  const cb = new TestableCircuitBreaker(2, 100);
  const fail = () => Promise.reject(new Error("fail"));
  const success = () => Promise.resolve("ok");

  await cb.call(fail).catch(() => {});
  await cb.call(fail).catch(() => {});
  console.log("State:", cb.state);

  await new Promise(r => setTimeout(r, 150));

  const result = await cb.call(success);
  console.log("Result:", result);
  console.log("State:", cb.state);
}

testHalfOpenCloses();
// State: open
// Result: ok
// State: closed

Integration Testing with Failure Injection

Integration tests verify the circuit breaker works with real HTTP clients and downstream services.

const http = require("http");

function createFailingServer(port) {
  let requestCount = 0;
  return http.createServer((req, res) => {
    requestCount++;
    if (requestCount <= 3) {
      res.writeHead(500);
      res.end("fail");
    } else {
      res.writeHead(200);
      res.end("ok");
    }
  }).listen(port);
}

async function testWithRealServer() {
  const server = createFailingServer(3000);
  const cb = new TestableCircuitBreaker(3, 500);

  for (let i = 0; i < 5; i++) {
    try {
      const result = await cb.call(() =>
        fetch("http://localhost:3000/test")
      );
      console.log("Call", i + 1, ":", result.status);
    } catch (err) {
      console.log("Call", i + 1, ": rejected", err.message);
    }
  }

  server.close();
}

Common Mistakes

  1. Testing with mocks that never match real behavior -- Mocked services don't simulate network latency, connection drops, or partial failures. Use real HTTP servers or chaos proxies in integration tests.

  2. Not testing the half-open transition -- The half-open state is the most complex and error-prone. Always test that a single success closes the circuit and a single failure reopens it.

  3. Testing without resetting state between cases -- Circuit breaker tests must start with a fresh instance. Shared state between test cases causes false failures or passes.

  4. Forgetting to test concurrent requests -- Race conditions in state transitions only appear under concurrent load. Add tests that fire multiple requests simultaneously.

  5. Not verifying log output -- State transitions must be logged. Test that logging occurs by capturing log output in your test assertions.

Practice Questions

  1. What three state transitions should every circuit breaker test cover? Closed to open (on threshold exceeded), open to half-open (after timeout), half-open to closed (on success) or open (on failure).

  2. How do you test that the circuit stays closed for transient failures below the threshold? Fire fewer than the threshold number of failures, then a success. Verify the state remains closed and the failure counter resets.

  3. Why should you use a real HTTP server for integration tests instead of mocks? Mocks don't simulate real network behavior like latency, connection resets, and partial response failures.

  4. Challenge: Write a test that verifies circuit breaker behavior under concurrent requests.

async function testConcurrentFailures() {
  const cb = new TestableCircuitBreaker(3, 5000);
  const fail = () => Promise.reject(new Error("fail"));

  const results = await Promise.allSettled(
    Array(5).fill(null).map(() => cb.call(fail))
  );

  results.forEach((r, i) => {
    console.log(`Request ${i + 1}:`, r.status);
  });
  console.log("Final state:", cb.state);
}

testConcurrentFailures();
// Requests 1-3: rejected (failures increment counter)
// Request 4-5: rejected with "open"
// Final state: open

FAQ

How do I test the half-open timeout without waiting in real time?

Override the nextAttempt timestamp in tests or inject a fake clock using sinon.useFakeTimers or vi.useFakeTimers.

Should I test every possible failure type?

Test the most common failures: timeout, connection refused, 500 status. Testing every possible error code is usually overkill.

What coverage threshold should I aim for?

Aim for 90%+ on state transition logic, 80%+ on integration paths. The circuit breaker is a critical infrastructure component.

How do I test circuit breaker metrics and monitoring?

Assert on emitted events or collected metrics after each test. Verify that open events, close events, and failure counts are recorded correctly.

Can I use property-based testing for circuit breakers?

Yes. Property-based testing can verify invariants: the circuit is always in exactly one state, the failure count is never negative, and transitions follow the state machine rules.

Mini Project

Build a comprehensive test suite for a circuit breaker that includes unit tests for state transitions, integration tests with a failing HTTP server, and a chaos test that randomly injects failures.

class CircuitBreakerTestSuite {
  constructor() {
    this.passed = 0;
    this.failed = 0;
  }

  async test(description, fn) {
    try {
      await fn();
      this.passed++;
      console.log("PASS:", description);
    } catch (err) {
      this.failed++;
      console.log("FAIL:", description, "-", err.message);
    }
  }

  async run() {
    const cb = new TestableCircuitBreaker(3, 100);

    await this.test("opens after threshold failures", async () => {
      for (let i = 0; i < 3; i++) {
        await cb.call(() => Promise.reject(new Error())).catch(() => {});
      }
      if (cb.state !== "open") throw new Error("state should be open");
    });

    const cb2 = new TestableCircuitBreaker(2, 50);
    await this.test("closes after half-open success", async () => {
      await cb2.call(() => Promise.reject(new Error())).catch(() => {});
      await cb2.call(() => Promise.reject(new Error())).catch(() => {});
      await new Promise(r => setTimeout(r, 100));
      await cb2.call(() => Promise.resolve("ok"));
      if (cb2.state !== "closed") throw new Error("state should be closed");
    });

    console.log(`\nResults: ${this.passed} passed, ${this.failed} failed`);
  }
}

const suite = new CircuitBreakerTestSuite();
suite.run();

What's Next

Now that you know how to test circuit breakers, learn about performance tuning circuit breakers for production workloads. Then explore best practices for circuit breaker deployment.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro