Unleash Feature Toggle Testing: A Complete Guide
Unleash is an open-source feature management platform used by thousands of teams to control feature rollouts without code deploys. But the flexibility that makes Unleash powerful — custom activation strategies, gradual rollouts, user segmentation — also makes testing more complex. This guide covers every layer of Unleash testing from SDK mocks to production monitoring.
Understanding Unleash's Testing Surface
Before writing tests, understand what can go wrong with feature toggles:
- Wrong default state — a toggle that should be off is on, or vice versa
- Broken activation strategy — your custom strategy returns incorrect results for edge-case contexts
- SDK integration bugs — the Unleash client isn't called where you expect
- Context propagation errors — user ID, session ID, or environment context isn't passed correctly
- Variant misconfiguration — multivariate toggles return wrong variant payloads
Testing must address all five failure modes.
Unit Testing with the Unleash Fake Client
The Unleash SDK ecosystem provides official fake/mock implementations for testing.
Node.js: FakeUnleashClient
const { FakeUnleashClient } = require('unleash-client/lib/unleash');
describe('Feature toggle behavior', () => {
let client;
beforeEach(() => {
client = new FakeUnleashClient();
});
it('shows legacy UI when toggle is off', () => {
client.setVariant('new-ui', { enabled: false, name: 'disabled' });
const feature = new FeatureService(client);
expect(feature.getUIVersion()).toBe('legacy');
});
it('shows new UI when toggle is on', () => {
client.setVariant('new-ui', { enabled: true, name: 'enabled' });
const feature = new FeatureService(client);
expect(feature.getUIVersion()).toBe('v2');
});
});Java: Unleash Mock with UnleashMock
import io.getunleash.FakeUnleash;
@Test
void shouldShowNewDashboard_whenToggleEnabled() {
FakeUnleash fakeUnleash = new FakeUnleash();
fakeUnleash.enable("new-dashboard");
DashboardService service = new DashboardService(fakeUnleash);
assertEquals("new-dashboard-v2", service.getDashboardVersion());
}
@Test
void shouldShowLegacyDashboard_whenToggleDisabled() {
FakeUnleash fakeUnleash = new FakeUnleash();
fakeUnleash.disableAll();
DashboardService service = new DashboardService(fakeUnleash);
assertEquals("dashboard-v1", service.getDashboardVersion());
}The FakeUnleash class is provided by the official unleash-client-java SDK. No network connection needed.
Testing Custom Activation Strategies
Unleash's power comes from custom activation strategies. These need dedicated unit tests.
Defining a Custom Strategy
class GradualRolloutStrategy {
isEnabled(parameters, context) {
const percentage = parseInt(parameters.percentage, 10);
const userId = context.userId;
const normalizedId = hashUserId(userId) % 100;
return normalizedId < percentage;
}
}Testing the Strategy in Isolation
describe('GradualRolloutStrategy', () => {
const strategy = new GradualRolloutStrategy();
it('enables for users within rollout percentage', () => {
// User whose hash % 100 = 15
const result = strategy.isEnabled(
{ percentage: '20' },
{ userId: 'deterministic-user-15' }
);
expect(result).toBe(true);
});
it('disables for users outside rollout percentage', () => {
// User whose hash % 100 = 85
const result = strategy.isEnabled(
{ percentage: '20' },
{ userId: 'deterministic-user-85' }
);
expect(result).toBe(false);
});
it('handles 0% rollout', () => {
const result = strategy.isEnabled(
{ percentage: '0' },
{ userId: 'any-user' }
);
expect(result).toBe(false);
});
it('handles 100% rollout', () => {
const result = strategy.isEnabled(
{ percentage: '100' },
{ userId: 'any-user' }
);
expect(result).toBe(true);
});
});Testing at 0%, 100%, and boundary values catches the most common strategy bugs.
Integration Testing Against a Local Unleash Instance
For integration tests, run Unleash locally using Docker:
# docker-compose.test.yml
services:
unleash:
image: unleashorg/unleash-server:latest
ports:
- "4242:4242"
environment:
DATABASE_URL: postgres://unleash:password@db/unleash
INIT_CLIENT_API_TOKENS: test-client-token
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_DB: unleash
POSTGRES_USER: unleash
POSTGRES_PASSWORD: passwordPre-seeding Toggle State
Use the Unleash Admin API to set toggle state before tests run:
# Create toggle
curl -X POST http://localhost:4242/api/admin/projects/default/features \
-H <span class="hljs-string">"Authorization: Bearer admin-token" \
-H <span class="hljs-string">"Content-Type: application/json" \
-d <span class="hljs-string">'{"name": "new-checkout", "type": "release", "enabled": true}'
<span class="hljs-comment"># Enable in development environment
curl -X POST \
<span class="hljs-string">"http://localhost:4242/api/admin/projects/default/features/new-checkout/environments/development/on" \
-H <span class="hljs-string">"Authorization: Bearer admin-token"Wrap this in a test setup script that runs before your integration test suite.
Testing Context Propagation
Context bugs — where user ID, session ID, or custom properties aren't passed to Unleash — are common and hard to spot. Test them explicitly.
describe('Unleash context propagation', () => {
it('passes user ID to toggle evaluation', async () => {
const calls = [];
const mockClient = {
isEnabled: (name, context) => {
calls.push({ name, context });
return true;
}
};
const service = new CheckoutService(mockClient);
await service.getCheckoutFlow({ userId: 'user-123', sessionId: 'sess-456' });
expect(calls[0].context).toMatchObject({
userId: 'user-123',
sessionId: 'sess-456',
});
});
});If your service doesn't pass context, Unleash can't evaluate user-specific rules — and the test will fail, surfacing the bug.
Testing Variant Payloads
Unleash variants carry JSON payloads for A/B experiments. Test that your code handles them:
describe('A/B test variant handling', () => {
it('renders button text from variant payload', () => {
client.setVariant('cta-button', {
enabled: true,
name: 'variant-b',
payload: { type: 'string', value: 'Get Started Free' }
});
const component = renderCTA(client);
expect(component.buttonText).toBe('Get Started Free');
});
it('uses default button text when variant payload is missing', () => {
client.setVariant('cta-button', {
enabled: false,
name: 'disabled',
});
const component = renderCTA(client);
expect(component.buttonText).toBe('Sign Up'); // default
});
});End-to-End Testing with Unleash
E2E tests must handle dynamic toggle state. Use these patterns:
Pattern 1: Dedicated E2E Toggle State
Create toggles specifically for E2E test scenarios and lock their state in the test environment:
Toggle: e2e-new-checkout-enabled
Environment: e2e
State: always onConfigure your E2E test user to hit the E2E environment.
Pattern 2: Toggle State Reset Between Tests
Before each E2E test, reset relevant toggles via the Admin API:
beforeEach(async () => {
await resetToggle('new-checkout', { enabled: false });
});
it('verifies checkout flow with legacy UI', async () => {
await browser.url('/checkout');
await expect($('.legacy-checkout')).toExist();
});Pattern 3: HelpMeTest Monitoring
Set up HelpMeTest to run your critical user journeys every few minutes. If a toggle change breaks a flow, you'll know within 5 minutes — not when a customer complains.
Health check: checkout-flow-with-new-toggle
Interval: 5 minutes
Test: complete purchase as authenticated user
Alert: Slack + email on failureCI/CD Pipeline Integration
Matrix Testing for Toggle States
# .github/workflows/test.yml
strategy:
matrix:
toggle_state: [enabled, disabled]
steps:
- name: Set toggle state
run: |
curl -X POST "$UNLEASH_URL/api/admin/features/new-checkout/environments/ci/${{ matrix.toggle_state }}" \
-H "Authorization: Bearer $UNLEASH_ADMIN_TOKEN"
- name: Run tests
run: npm test
env:
UNLEASH_URL: ${{ secrets.UNLEASH_CI_URL }}
UNLEASH_TOKEN: ${{ secrets.UNLEASH_CI_TOKEN }}Toggle Drift Detection
Compare your code's toggle references against what's defined in Unleash:
// scripts/check-toggle-drift.js
const usedToggles = extractToggleRefs('./src'); // grep for isEnabled calls
const definedToggles = await fetchUnleashToggles();
const undefined_toggles = usedToggles.filter(t => !definedToggles.includes(t));
if (undefined_toggles.length > 0) {
console.error('Toggles used in code but not defined in Unleash:', undefined_toggles);
process.exit(1);
}Run this check in CI to catch missing toggle definitions before deploy.
Testing Toggle Cleanup
Technical debt accumulates around stale toggles. Build cleanup testing into your process:
- Mark toggles as permanent vs. temporary using Unleash's toggle types (
release,experiment,operational,kill-switch,permission) - Set expiry dates on release toggles — Unleash shows warnings for expired toggles
- Test the "cleaned up" state before removing toggle references from code: deploy the code as if the toggle doesn't exist, verify behavior
// Cleanup test: verify code works without toggle evaluation
it('works without feature toggle dependency', () => {
// Remove toggle check — simulates post-cleanup state
const feature = new FeatureService(null); // no client
expect(feature.getUIVersion()).toBe('v2'); // new UI should be default after cleanup
});Summary
Testing Unleash feature toggles requires: SDK-level unit tests using FakeUnleash or FakeUnleashClient, strategy unit tests covering edge cases, integration tests against a local Unleash instance with pre-seeded state, explicit context propagation tests, and E2E tests that lock toggle state per test. Add toggle drift detection to CI and monitoring in production, and you'll catch toggle-related regressions before they reach users.