Unleash Feature Flags: Testing Toggles in Node.js and Python Applications

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 UnleashClient

The Testing Problem

The standard Unleash client polls a remote server for flag states. In tests, this means:

  1. External network dependency
  2. Non-deterministic results (flags can change)
  3. 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: unleash

Then 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.

Read more