Testing Feature Flags with Flagsmith and Jest: Targeting Rules and Local Evaluation

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

Use 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 config

If 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 push

End-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

Read more

Testing Atlantis Terraform PR Automation: Workflows, Plan Verification, and Policy Enforcement

Testing Atlantis Terraform PR Automation: Workflows, Plan Verification, and Policy Enforcement

Atlantis automates Terraform plan and apply through pull requests. But Atlantis itself needs testing: workflow configuration, plan output validation, policy enforcement, and server health checks. This guide covers testing Atlantis workflows locally with atlantis-local, validating plan outputs with custom scripts, enforcing Terraform policies with OPA and Conftest, and monitoring Atlantis

By HelpMeTest