Testing Feature Flags with Flagsmith and Jest: Targeting Rules and Local Evaluation
Flagsmith supports local evaluation mode where flag rules are downloaded once and evaluated in-process. This makes it ideal for testing—no HTTP calls during test runs. This guide covers testing flag evaluation, targeting rules, percentage rollouts, and identity overrides in Jest without a running Flagsmith instance.
Why Flagsmith Is Testable
Flagsmith's server-side SDKs support local evaluation: the SDK downloads the full environment configuration (flags, segments, targeting rules) from the Flagsmith API and evaluates flags in-process. This means:
- No network call per flag check during test execution
- Deterministic results for the same identity + configuration
- Offline testing with static configuration files
- Fast unit tests without HTTP mocking
SDK Setup
npm install flagsmith-nodejs// src/feature-flags.js
const Flagsmith = require('flagsmith-nodejs');
let flagsmith;
async function initFlagsmith(identity = null) {
flagsmith = new Flagsmith({
environmentKey: process.env.FLAGSMITH_SERVER_KEY,
enableLocalEvaluation: true, // Download rules, evaluate in-process
environmentRefreshIntervalSeconds: 60,
});
await flagsmith.init();
return flagsmith;
}
async function getFlags(identity, traits = {}) {
if (identity) {
return flagsmith.getIdentityFlags(identity, traits);
}
return flagsmith.getEnvironmentFlags();
}
module.exports = { initFlagsmith, getFlags };Testing With a Static Environment Configuration
For tests, load a static JSON configuration instead of calling the Flagsmith API. Export your environment's configuration from the Flagsmith dashboard:
# Export via Flagsmith API
curl -H <span class="hljs-string">"X-Environment-Key: $FLAGSMITH_SERVER_KEY" \
https://edge.api.flagsmith.com/api/v1/environment-document/ \
> <span class="hljs-built_in">test/fixtures/flagsmith-environment.jsonUse this in tests:
// test/helpers/flagsmith-test.js
const Flagsmith = require('flagsmith-nodejs');
const environmentDocument = require('../fixtures/flagsmith-environment.json');
async function createTestFlagsmith(identityOverrides = {}) {
const flagsmith = new Flagsmith({
environmentKey: 'test',
enableLocalEvaluation: true,
customHeaders: {},
});
// Bypass network fetch; inject the static environment document
flagsmith.environment = flagsmith.environmentParser(environmentDocument);
return flagsmith;
}
module.exports = { createTestFlagsmith };Unit Testing Flag Evaluation
// test/feature-flags.test.js
const { createTestFlagsmith } = require('./helpers/flagsmith-test');
let flagsmith;
beforeAll(async () => {
flagsmith = await createTestFlagsmith();
});
describe('flag evaluation', () => {
it('returns correct value for a boolean flag', async () => {
const flags = await flagsmith.getEnvironmentFlags();
const value = flags.isFeatureEnabled('dark_mode');
// Assert against what's in your fixture file
expect(typeof value).toBe('boolean');
});
it('returns string value for a remote config flag', async () => {
const flags = await flagsmith.getEnvironmentFlags();
const buttonText = flags.getFeatureValue('checkout_button_text', 'Checkout');
expect(typeof buttonText).toBe('string');
expect(buttonText.length).toBeGreaterThan(0);
});
});Testing Targeting Rules
Targeting rules evaluate flags differently based on user traits (plan, country, account age). Test each rule path explicitly:
// test/targeting-rules.test.js
const { createTestFlagsmith } = require('./helpers/flagsmith-test');
// Assume the environment document has these rules configured:
// Feature: "premium_analytics"
// Rule 1: If trait "plan" is "enterprise" → enabled=true
// Rule 2: If trait "plan" is "pro" → enabled=true, value="limited"
// Default: enabled=false
let flagsmith;
beforeAll(async () => {
flagsmith = await createTestFlagsmith();
});
describe('premium_analytics targeting rules', () => {
it('enables for enterprise users', async () => {
const flags = await flagsmith.getIdentityFlags(
'enterprise_user_123',
{ plan: 'enterprise' }
);
expect(flags.isFeatureEnabled('premium_analytics')).toBe(true);
expect(flags.getFeatureValue('premium_analytics', null)).toBe('full');
});
it('enables limited version for pro users', async () => {
const flags = await flagsmith.getIdentityFlags(
'pro_user_456',
{ plan: 'pro' }
);
expect(flags.isFeatureEnabled('premium_analytics')).toBe(true);
expect(flags.getFeatureValue('premium_analytics', null)).toBe('limited');
});
it('disables for free users', async () => {
const flags = await flagsmith.getIdentityFlags(
'free_user_789',
{ plan: 'free' }
);
expect(flags.isFeatureEnabled('premium_analytics')).toBe(false);
});
it('disables when plan trait is absent', async () => {
const flags = await flagsmith.getIdentityFlags('unknown_user', {});
expect(flags.isFeatureEnabled('premium_analytics')).toBe(false);
});
});Testing Percentage Rollouts
Flagsmith supports percentage rollouts by hashing the identity. Test that the rollout bucket is deterministic and the distribution is correct:
// test/percentage-rollout.test.js
const { createTestFlagsmith } = require('./helpers/flagsmith-test');
// Assume "beta_dashboard" has a 20% rollout configured in the fixture
describe('beta_dashboard percentage rollout', () => {
it('assigns same user to same bucket consistently', async () => {
const flagsmith = await createTestFlagsmith();
const userId = 'consistent_user_abc';
const flags1 = await flagsmith.getIdentityFlags(userId, {});
const flags2 = await flagsmith.getIdentityFlags(userId, {});
const result1 = flags1.isFeatureEnabled('beta_dashboard');
const result2 = flags2.isFeatureEnabled('beta_dashboard');
expect(result1).toBe(result2);
});
it('includes approximately 20% of users in the rollout', async () => {
const flagsmith = await createTestFlagsmith();
let enabled = 0;
const total = 1000;
for (let i = 0; i < total; i++) {
const flags = await flagsmith.getIdentityFlags(`user_${i}`, {});
if (flags.isFeatureEnabled('beta_dashboard')) enabled++;
}
// Allow ±5% tolerance
const percentage = enabled / total;
expect(percentage).toBeGreaterThan(0.15);
expect(percentage).toBeLessThan(0.25);
});
});Testing Identity Overrides
Flagsmith allows per-identity flag overrides that bypass rules. Test that your code handles these correctly:
// test/identity-overrides.test.js
const Flagsmith = require('flagsmith-nodejs');
const environmentDocument = require('../fixtures/flagsmith-environment.json');
it('identity-level override takes precedence over segment rules', async () => {
const flagsmith = new Flagsmith({ environmentKey: 'test', enableLocalEvaluation: true });
flagsmith.environment = flagsmith.environmentParser(environmentDocument);
// The fixture should have an identity override for "vip_user_001"
// that enables "new_feature" even though the segment rule would disable it
const flags = await flagsmith.getIdentityFlags('vip_user_001', { plan: 'free' });
// Despite being a free user (which would normally disable the flag),
// the identity override enables it
expect(flags.isFeatureEnabled('new_feature')).toBe(true);
});Mocking for Fast Unit Tests
If you don't want to depend on the environment document fixture, mock the Flagsmith client entirely:
// test/service-using-flags.test.js
jest.mock('flagsmith-nodejs', () => {
class MockFlagsmith {
async init() {}
async getEnvironmentFlags() {
return this._makeFlags({
dark_mode: { enabled: true, value: null },
checkout_v2: { enabled: false, value: null },
api_version: { enabled: true, value: 'v2' },
});
}
async getIdentityFlags(identity, traits = {}) {
return this._makeFlags({
premium_analytics: { enabled: traits.plan === 'enterprise', value: null },
});
}
_makeFlags(flagMap) {
return {
isFeatureEnabled: (key) => flagMap[key]?.enabled ?? false,
getFeatureValue: (key, defaultVal) => flagMap[key]?.value ?? defaultVal,
};
}
}
return MockFlagsmith;
});
const Flagsmith = require('flagsmith-nodejs');
const { DashboardService } = require('../src/dashboard-service');
it('shows premium analytics to enterprise users', async () => {
const service = new DashboardService(new Flagsmith());
const result = await service.getDashboardConfig('user_123', { plan: 'enterprise' });
expect(result.showAnalytics).toBe(true);
});
it('hides premium analytics for free users', async () => {
const service = new DashboardService(new Flagsmith());
const result = await service.getDashboardConfig('user_456', { plan: 'free' });
expect(result.showAnalytics).toBe(false);
});CI Integration
# .github/workflows/test.yml
- name: Run tests
run: npm test
env:
FLAGSMITH_SERVER_KEY: ${{ secrets.FLAGSMITH_SERVER_KEY }}
# Optional: use a test environment key that maps to a fixtures-aligned configIf you use the fixture file approach (static JSON), no Flagsmith API key is needed in CI at all—just commit the flagsmith-environment.json fixture and the tests run fully offline.
Keeping Fixtures Up to Date
The fixture file is a snapshot of your Flagsmith environment. Schedule a daily refresh in CI:
# .github/workflows/refresh-flagsmith-fixture.yml
name: Refresh Flagsmith Environment Fixture
on:
schedule:
- cron: '0 5 * * *'
jobs:
refresh:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download environment document
run: |
curl -H "X-Environment-Key: ${{ secrets.FLAGSMITH_SERVER_KEY }}" \
https://edge.api.flagsmith.com/api/v1/environment-document/ \
-o test/fixtures/flagsmith-environment.json
- name: Commit if changed
run: |
git diff --quiet test/fixtures/ || \
(git config user.email ci@yourapp.com && \
git config user.name "CI" && \
git add test/fixtures/flagsmith-environment.json && \
git commit -m "chore: refresh Flagsmith environment fixture")
git pushEnd-to-End Testing With HelpMeTest
Local evaluation tests cover flag logic. For browser-level verification—the right feature UI renders for the right user attributes—HelpMeTest lets you write plain-English tests that simulate user sessions with specific trait values and assert on the resulting page state, without maintaining complex Playwright fixture injection setups.
Summary
- Flagsmith's local evaluation mode downloads rules once and evaluates in-process—no HTTP per check
- Export your environment document as a JSON fixture for fully offline tests
- Test each targeting rule branch: enterprise user, pro user, free user, missing trait
- Test percentage rollout determinism and distribution with large sample assertions
- Mock the SDK entirely for fast unit tests that don't need rule evaluation
- Schedule daily fixture refresh to keep tests aligned with your live Flagsmith configuration