Unleash Feature Flags: Testing Toggles in Node.js and Python Applications
Unleash is an open-source feature flag management platform. It supports feature toggles with complex activation strategies (gradual rollout, user targeting, environment-based). Testing with Unleash requires either mocking the client or using the in-process client for integration tests.
SDK Setup
# Node.js
npm install unleash-client
<span class="hljs-comment"># Python
pip install UnleashClientThe Testing Problem
The standard Unleash client polls a remote server for flag states. In tests, this means:
- External network dependency
- Non-deterministic results (flags can change)
- Slow test startup (waiting for initial fetch)
Node.js: Mocking the Client
// feature-flags.js
import { initialize } from 'unleash-client'
let client = null
export async function initFlags() {
client = initialize({
url: process.env.UNLEASH_URL,
appName: 'my-app',
customHeaders: { Authorization: process.env.UNLEASH_TOKEN },
})
await client.start()
return client
}
export function isEnabled(flagName, context = {}) {
return client?.isEnabled(flagName, context) ?? false
}
export function getVariant(flagName, context = {}) {
return client?.getVariant(flagName, context)
}// feature-flags.test.js
import { vi, describe, test, expect, beforeEach } from 'vitest'
vi.mock('unleash-client', () => ({
initialize: vi.fn().mockReturnValue({
start: vi.fn().mockResolvedValue(undefined),
isEnabled: vi.fn(),
getVariant: vi.fn(),
})
}))
import { initialize } from 'unleash-client'
import { initFlags, isEnabled, getVariant } from './feature-flags'
let mockClient
beforeEach(async () => {
vi.clearAllMocks()
mockClient = initialize()
await initFlags()
})
describe('isEnabled', () => {
test('returns true when flag is enabled', () => {
mockClient.isEnabled.mockReturnValue(true)
expect(isEnabled('new-dashboard')).toBe(true)
expect(mockClient.isEnabled).toHaveBeenCalledWith('new-dashboard', {})
})
test('returns false when flag is disabled', () => {
mockClient.isEnabled.mockReturnValue(false)
expect(isEnabled('new-dashboard')).toBe(false)
})
test('passes user context to isEnabled', () => {
const context = { userId: 'user-123', properties: { plan: 'pro' } }
isEnabled('pro-feature', context)
expect(mockClient.isEnabled).toHaveBeenCalledWith('pro-feature', context)
})
})
describe('getVariant', () => {
test('returns correct variant', () => {
mockClient.getVariant.mockReturnValue({
name: 'blue',
enabled: true,
payload: { type: 'string', value: '#0000ff' }
})
const variant = getVariant('button-color')
expect(variant.name).toBe('blue')
expect(variant.payload.value).toBe('#0000ff')
})
})Integration Testing with In-Process Toggle
For integration tests, use Unleash's in-memory client. This avoids network calls while still exercising real toggle evaluation logic.
// test-helpers/unleash.js
import { InMemToggleRepository, UnleashClient } from 'unleash-client'
export function createTestClient(toggles = {}) {
const repository = new InMemToggleRepository()
// Register test toggles
for (const [name, enabled] of Object.entries(toggles)) {
repository.set(name, {
name,
enabled: true,
strategies: [
{
name: enabled ? 'default' : 'default',
parameters: {},
}
],
})
}
return new UnleashClient({
repository,
strategies: [],
backup: null,
url: 'http://localhost',
appName: 'test',
instanceId: 'test',
})
}Python Testing
# feature_service.py
from UnleashClient import UnleashClient
_client = None
def init_client():
global _client
_client = UnleashClient(
url=os.environ['UNLEASH_URL'],
app_name='my-app',
custom_headers={'Authorization': os.environ['UNLEASH_TOKEN']}
)
_client.initialize_client()
def is_enabled(flag_name: str, context: dict = None) -> bool:
if _client is None:
return False
return _client.is_enabled(flag_name, context or {})# test_feature_service.py
from unittest.mock import patch, MagicMock
import pytest
from feature_service import is_enabled
@pytest.fixture
def mock_unleash():
with patch('feature_service._client') as mock:
yield mock
def test_flag_enabled(mock_unleash):
mock_unleash.is_enabled.return_value = True
assert is_enabled('new-checkout') is True
mock_unleash.is_enabled.assert_called_once_with('new-checkout', {})
def test_flag_disabled(mock_unleash):
mock_unleash.is_enabled.return_value = False
assert is_enabled('new-checkout') is False
def test_passes_user_context(mock_unleash):
mock_unleash.is_enabled.return_value = True
context = {'userId': 'user-123'}
is_enabled('pro-feature', context)
mock_unleash.is_enabled.assert_called_with('pro-feature', context)Testing Gradual Rollout Logic
Gradual rollout uses a hash of userId + toggleName to determine inclusion. Test boundary conditions:
test('gradual rollout assigns consistently per user', () => {
const toggleName = 'dark-mode'
const rolloutPercent = 50
const results = new Map()
// Test 1000 users
for (let i = 0; i < 1000; i++) {
const userId = `user-${i}`
const enabled = isInRollout(userId, toggleName, rolloutPercent)
results.set(userId, enabled)
}
// Same user always gets same result
for (const [userId, result] of results) {
expect(isInRollout(userId, toggleName, rolloutPercent)).toBe(result)
}
// ~50% should be enabled
const enabledCount = [...results.values()].filter(Boolean).length
expect(enabledCount).toBeGreaterThan(400)
expect(enabledCount).toBeLessThan(600)
})Testing Middleware Integration
// middleware/feature-flags.js
export function featureFlagMiddleware(req, res, next) {
req.features = {
newDashboard: isEnabled('new-dashboard', { userId: req.user?.id }),
betaFeatures: isEnabled('beta-features', { userId: req.user?.id }),
}
next()
}
// middleware/feature-flags.test.js
import { isEnabled } from '../feature-flags'
vi.mock('../feature-flags')
test('middleware attaches feature flags to request', async () => {
isEnabled.mockImplementation((flag) => flag === 'new-dashboard')
const req = { user: { id: 'user-123' } }
const res = {}
const next = vi.fn()
featureFlagMiddleware(req, res, next)
expect(req.features.newDashboard).toBe(true)
expect(req.features.betaFeatures).toBe(false)
expect(next).toHaveBeenCalled()
})Self-Hosted Unleash for Integration Tests
Run a real Unleash instance in Docker for integration tests:
# docker-compose.test.yml
services:
unleash:
image: unleashorg/unleash-server
environment:
DATABASE_URL: postgres://unleash:unleash@db/unleash
INIT_FRONTEND_API_TOKENS: test-token
ports:
- "4242:4242"
depends_on:
- db
db:
image: postgres:15
environment:
POSTGRES_DB: unleash
POSTGRES_USER: unleash
POSTGRES_PASSWORD: unleashThen seed flags via the API before tests run:
# Create a feature flag
curl -X POST http://localhost:4242/api/admin/features \
-H <span class="hljs-string">'Authorization: test-token' \
-H <span class="hljs-string">'Content-Type: application/json' \
-d <span class="hljs-string">'{"name":"new-checkout","enabled":true,"strategies":[{"name":"default"}]}'Summary
For unit tests: mock the Unleash client entirely. For integration tests: either use the in-process InMemToggleRepository or run a real Unleash instance in Docker. The key principle is the same as all feature flag testing — make toggle state explicit and deterministic per test, never rely on a live server returning a specific state.