Overview
LaunchDarkly's server-side SDK exposes a TestData data source that lets tests configure flag values without any network call - the canonical hermetic-test pattern. Per launchdarkly.com/docs/sdk (opens in new window), TestData replaces the production data source; everything else about the SDK (variation evaluation, targeting rules, default handling) runs the real code paths.
When to use
Authoring
Install
npm install --save-dev launchdarkly-node-server-sdk
pip install launchdarkly-server-sdkInitialize with TestData
import * as LaunchDarkly from 'launchdarkly-node-server-sdk';
const td = LaunchDarkly.TestData.dataSource();
const client = LaunchDarkly.init('sdk-test-key', {
updateProcessor: td,
sendEvents: false, // No event uploads in tests
});
await client.waitForInitialization();
// Configure a flag's default + per-user variation
td.update(td.flag('show-new-ui').booleanFlag().on(true));Variation evaluation
test('flag on returns true', async () => {
td.update(td.flag('show-new-ui').booleanFlag().on(true));
const user = { key: 'user-1' };
const enabled = await client.variation('show-new-ui', user, false);
expect(enabled).toBe(true);
});
test('flag off returns default', async () => {
td.update(td.flag('show-new-ui').booleanFlag().on(false));
const enabled = await client.variation('show-new-ui', { key: 'user-1' }, false);
expect(enabled).toBe(false);
});Targeting rules
test('only premium users get treatment', async () => {
td.update(
td.flag('premium-feature')
.booleanFlag()
.variationForUser('premium-user-1', true)
.fallthroughVariation(false)
);
expect(await client.variation('premium-feature', { key: 'premium-user-1' }, false)).toBe(true);
expect(await client.variation('premium-feature', { key: 'free-user-1' }, false)).toBe(false);
});Percentage rollout (deterministic assignment)
test('rollout is deterministic per user', async () => {
td.update(td.flag('half-rollout').booleanFlag().on(true).variations(true, false));
// Bucketing is deterministic on user key
const r1 = await client.variation('half-rollout', { key: 'user-1' }, false);
const r2 = await client.variation('half-rollout', { key: 'user-1' }, false);
expect(r1).toBe(r2);
});File-based data source (alternative)
For fixture-shared tests:
const fileSource = LaunchDarkly.FileDataSource({
paths: ['./tests/fixtures/ld-flags.json'],
});
const client = LaunchDarkly.init('sdk-test-key', {
updateProcessor: fileSource,
sendEvents: false,
});The JSON file uses LaunchDarkly's flag-snapshot format. Useful for cross-language test fixtures.
Teardown
afterAll(async () => {
await client.close();
});Running
npm testCI integration
jobs:
ld-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v4
- run: npm ci && npm testNo LAUNCHDARKLY_SDK_KEY needed in CI - TestData/file source replaces it.
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Real SDK key in tests | Test traffic pollutes prod analytics | TestData / FileDataSource |
sendEvents: true in tests | Event upload from CI | sendEvents: false |
| Multiple init per test | Slow init; leaks | One global client; TestData.update per test |
Asserting exact variation result without td.update | Race vs SDK state | Always update first |
Forgetting await client.waitForInitialization() | Race; variation returns default | Always wait |
Sharing td across test files | Cross-test pollution | Per-file or per-test TestData |
No teardown client.close() | Network handles leak | Always close in afterAll |
Limitations
References
- Dmitri's RFC needs an answer by Thursday and the decision is mine
- Second pair of eyes on PR 2291 before Tuesday's branch cut
- Marta's branch merges at 16:00 and I need the flag bill sorted, not the job
- Re-recording the nav bucket values before Thursday's freeze
- Ivan's retry patch goes in Monday unless somebody comes back by Friday