The ToxiproxyHook provides a Toxiproxy container for testing network conditions and service degradation. Toxiproxy allows you to simulate various network problems like latency, bandwidth limits, connection failures, and more.
The hook only runs when explicitly configured by calling useToxiproxy() in the setup phase. If you don't call this function, the hook is inactive and won't affect your test lifecycle.
- Network Condition Simulation: Test latency, bandwidth limits, timeouts, and connection failures
- Multiple Proxies: Configure multiple service proxies in a single container
- Dynamic Toxic Management: Add, remove, enable, and disable toxics at runtime
- Directional Toxics: Apply toxics to upstream or downstream traffic independently
- Toxicity Probability: Configure toxics to apply only to a percentage of connections
- No Service Modification: Test network resilience without modifying your services
- Opt-in Activation: Only runs when explicitly configured
Toxiproxy is a framework for simulating network conditions. It's particularly useful for testing:
- How your application handles slow network connections
- Behavior under bandwidth constraints
- Recovery from connection failures
- Timeout handling
- Packet loss and corruption scenarios
pnpm add -D @testcontainers/toxiproxy toxiproxy-node-clientimport {describe, it} from "node:test";
import assert from "node:assert/strict";
import {useNodeBoot} from "@nodeboot/node-test";
import {EmptyApp} from "../src/empty-app";
describe("Toxiproxy Test", () => {
const {useToxiproxy} = useNodeBoot(EmptyApp, ({useToxiproxy}) => {
useToxiproxy(); // Activates the hook
});
it("should have Toxiproxy running", () => {
const {host, controlPort} = useToxiproxy();
console.log(`Toxiproxy at ${host}:${controlPort}`);
assert.ok(host);
assert.ok(controlPort > 0);
});
});Create a proxy without any toxics:
describe("Simple Redis Proxy", () => {
const {useToxiproxy} = useNodeBoot(EmptyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [
{
name: "redis",
upstream: "localhost:6379",
},
],
});
});
it("should proxy to Redis", () => {
const {getProxy} = useToxiproxy();
const redis = getProxy("redis");
// Connect to redis.host:redis.port instead of localhost:6379
console.log(`Connect to Redis via: ${redis.host}:${redis.port}`);
});
});Add network latency to simulate slow connections:
describe("Slow Network", () => {
const {useToxiproxy} = useNodeBoot(EmptyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [
{
name: "slow-api",
upstream: "localhost:3000",
toxics: [
{
type: "latency",
stream: "downstream",
attributes: {
latency: 1000, // 1 second delay
jitter: 200, // ±200ms variation
},
},
],
},
],
});
});
it("should add 1s latency to responses", async () => {
// Test your application's behavior with slow responses
});
});Limit connection bandwidth:
describe("Limited Bandwidth", () => {
const {useToxiproxy} = useNodeBoot(EmptyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [
{
name: "limited-api",
upstream: "localhost:3000",
toxics: [
{
type: "bandwidth",
stream: "downstream",
attributes: {
rate: 100, // 100 KB/s
},
},
],
},
],
});
});
});Simulate connection timeouts:
describe("Connection Timeout", () => {
const {useToxiproxy} = useNodeBoot(EmptyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [
{
name: "timeout-api",
upstream: "localhost:3000",
toxics: [
{
type: "timeout",
stream: "downstream",
attributes: {
timeout: 5000, // 5 second timeout
},
},
],
},
],
});
});
});Simulate complete service outage:
describe("Service Outage", () => {
const {useToxiproxy} = useNodeBoot(EmptyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [
{
name: "down-api",
upstream: "localhost:3000",
toxics: [
{
type: "down",
stream: "downstream",
toxicity: 1.0, // 100% of connections fail
},
],
},
],
});
});
});Adds delay to connections.
{
type: "latency",
stream: "downstream",
attributes: {
latency: 1000, // Delay in milliseconds
jitter: 100 // Random variation (±ms)
}
}Closes connections immediately.
{
type: "down",
stream: "downstream",
toxicity: 1.0 // Probability (0.0 - 1.0)
}Limits connection bandwidth.
{
type: "bandwidth",
stream: "downstream",
attributes: {
rate: 100 // KB/s
}
}Delays closing the connection.
{
type: "slow_close",
stream: "downstream",
attributes: {
delay: 2000 // Milliseconds
}
}Stops data transfer after timeout.
{
type: "timeout",
stream: "downstream",
attributes: {
timeout: 5000 // Milliseconds
}
}Slices TCP packets into smaller chunks.
{
type: "slicer",
stream: "downstream",
attributes: {
average_size: 64, // Bytes
size_variation: 32, // Bytes
delay: 10 // Microseconds between slices
}
}Limits total data transferred.
{
type: "limit_data",
stream: "downstream",
attributes: {
bytes: 1024 // Total bytes before closing
}
}describe("Dynamic Toxics", () => {
const {useToxiproxy} = useNodeBoot(EmptyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [{name: "api", upstream: "localhost:3000"}],
});
});
it("should add latency dynamically", async () => {
const {addToxic} = useToxiproxy();
// Add latency during test
await addToxic("api", {
type: "latency",
stream: "downstream",
name: "test-latency",
attributes: {latency: 500},
});
});
});it("should remove toxic", async () => {
const {removeToxic} = useToxiproxy();
await removeToxic("api", "test-latency");
});it("should disable proxy", async () => {
const {disableProxy} = useToxiproxy();
await disableProxy("api"); // All connections will fail
});
it("should enable proxy", async () => {
const {enableProxy} = useToxiproxy();
await enableProxy("api"); // Restore connections
});Use toxicity probability for realistic scenarios:
{
type: "latency",
stream: "downstream",
toxicity: 0.3, // Only 30% of requests affected
attributes: {
latency: 2000
}
}Apply different toxics to upstream and downstream:
toxics: [
{
type: "latency",
stream: "upstream",
name: "request-latency",
attributes: {latency: 100},
},
{
type: "bandwidth",
stream: "downstream",
name: "response-limit",
attributes: {rate: 50},
},
];Test interactions between multiple services:
useToxiproxy({
proxies: [
{
name: "redis",
upstream: "localhost:6379",
toxics: [{type: "latency", stream: "downstream", attributes: {latency: 50}}],
},
{
name: "postgres",
upstream: "localhost:5432",
toxics: [{type: "latency", stream: "downstream", attributes: {latency: 100}}],
},
{
name: "api",
upstream: "localhost:3000",
toxics: [{type: "bandwidth", stream: "downstream", attributes: {rate: 100}}],
},
],
});describe("Circuit Breaker Test", () => {
const {useToxiproxy} = useNodeBoot(MyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [
{
name: "flaky-api",
upstream: "localhost:3000",
toxics: [
{
type: "timeout",
stream: "downstream",
toxicity: 0.5, // 50% failure rate
attributes: {timeout: 1000},
},
],
},
],
});
});
it("should open circuit after failures", async () => {
// Test that circuit breaker opens after threshold
});
});describe("Retry Test", () => {
const {useToxiproxy} = useNodeBoot(MyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [
{
name: "unreliable-api",
upstream: "localhost:3000",
toxics: [
{
type: "down",
stream: "downstream",
toxicity: 0.7, // 70% failure rate
},
],
},
],
});
});
it("should retry failed requests", async () => {
// Test retry behavior
});
});describe("Timeout Handling", () => {
const {useToxiproxy} = useNodeBoot(MyApp, ({useToxiproxy}) => {
useToxiproxy({
proxies: [
{
name: "slow-api",
upstream: "localhost:3000",
toxics: [
{
type: "latency",
stream: "downstream",
attributes: {latency: 10000}, // 10 seconds
},
],
},
],
});
});
it("should timeout and handle gracefully", async () => {
// Test timeout behavior
});
});The hook sets these environment variables:
process.env["TOXIPROXY_HOST"]; // Container host
process.env["TOXIPROXY_PORT"]; // Control API portSetup Phase (if useToxiproxy() is called):
└─ Hook state set to enabled
beforeAll (if enabled):
├─ Detect and configure container runtime
├─ Start Toxiproxy container
├─ Configure proxies
├─ Apply toxics
└─ Set environment variables
Test Execution:
└─ Proxies available via useToxiproxy()
afterAll (if enabled):
└─ Stop Toxiproxy container and cleanup
If useToxiproxy() is NOT called:
└─ Hook remains inactive
export type ToxicType = "latency" | "down" | "bandwidth" | "slow_close" | "timeout" | "slicer" | "limit_data";
export type ToxicDirection = "upstream" | "downstream";
export type ToxicConfig = {
type: ToxicType;
stream: ToxicDirection;
name?: string;
toxicity?: number; // 0.0 - 1.0
attributes?: Record<string, any>;
};
export type ProxyConfig = {
name: string;
upstream: string;
enabled?: boolean;
toxics?: ToxicConfig[];
};
export type ToxiproxyOptions = {
image?: string;
proxies?: ProxyConfig[];
containerLogging?: boolean;
};{
container: StartedToxiProxyContainer;
host: string;
controlPort: number;
proxies: Map<string, ProxyInfo>;
getProxy: (name: string) => ProxyInfo | undefined;
addToxic: (proxyName: string, toxic: ToxicConfig) => Promise<void>;
removeToxic: (proxyName: string, toxicName: string) => Promise<void>;
enableProxy: (proxyName: string) => Promise<void>;
disableProxy: (proxyName: string) => Promise<void>;
}- Start Simple: Begin with basic toxics (latency, down) before complex scenarios
- Realistic Values: Use realistic network conditions based on your deployment environment
- Test Recovery: Focus on testing how your application recovers, not just fails
- Isolate Toxics: Test one toxic at a time initially
- Document Scenarios: Clearly document what network condition each test simulates
useToxiproxy({
proxies: [
{
name: "payment-gateway",
upstream: "payment-api:443",
toxics: [
{
type: "latency",
stream: "downstream",
attributes: {latency: 3000, jitter: 1000},
},
],
},
],
});useToxiproxy({
proxies: [
{
name: "database",
upstream: "postgres:5432",
toxics: [
{
type: "timeout",
stream: "downstream",
toxicity: 0.1, // 10% of queries timeout
attributes: {timeout: 5000},
},
],
},
],
});useToxiproxy({
proxies: [
{
name: "rate-limited-api",
upstream: "api:3000",
toxics: [
{
type: "limit_data",
stream: "downstream",
attributes: {bytes: 10240}, // 10KB limit
},
],
},
],
});Ensure you're connecting to the proxied port:
const {getProxy} = useToxiproxy();
const proxy = getProxy("redis");
// Connect to proxy.host:proxy.port, NOT localhost:6379Check stream direction (upstream vs downstream) and toxicity probability.
Ensure Docker is running and testcontainers is properly configured.
- GenericContainerHook - For general container needs
- MongoContainerHook - MongoDB-specific testing
- Toxiproxy GitHub - Official documentation
- Testcontainers Toxiproxy - Module docs