Testing Circuit Breakers — Complete Implementation Guide
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
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.
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.
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.
Forgetting to test concurrent requests -- Race conditions in state transitions only appear under concurrent load. Add tests that fire multiple requests simultaneously.
Not verifying log output -- State transitions must be logged. Test that logging occurs by capturing log output in your test assertions.
Practice Questions
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).
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.
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.
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
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