CtrlK
BlogDocsLog inGet started
Tessl Logo
PluginPublic

testland/openfeature-sdk-testing

Use whenever code that reads feature flags through OpenFeature, Unleash, Flagsmith or GrowthBook needs tests: InMemoryProvider for hermetic tests, evaluation details, hooks and targeting context in TypeScript, Java and Python, plus a flag test-matrix workflow. LaunchDarkly has launchdarkly-testing.

Quality

93%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Overview
Quality
Evals
Security
Files

growthbook.mdreferences/

GrowthBook SDK testing

Wraps GrowthBook Node SDK testing patterns: GrowthBookClient initialization with direct payload (initSync; no network), isOn / getFeatureValue / evalFeature, scoped instances (createScopedInstance) for per-request user context, inline experiment (runInlineExperiment) tests, and tracking-callback assertion patterns.

Per docs.growthbook.io/lib/node, the GrowthBook Node SDK's GrowthBookClient supports an initSync({ payload }) pattern that fully bypasses the network - pass the feature definitions directly. createScopedInstance lets each test (or request) bind its own user context cleanly.

Install

npm install --save-dev @growthbook/growthbook

Initialize with payload (offline)

Per docs.growthbook.io:

import { GrowthBookClient } from '@growthbook/growthbook';

const gbClient = new GrowthBookClient().initSync({
  payload: {
    features: {
      'show-new-ui': { defaultValue: true },
      'experiment-x': {
        defaultValue: false,
        rules: [{ condition: { id: 'test-user-1' }, force: true }],
      },
    },
  },
});

initSync is purpose-built for tests - no async wait.

isOn / getFeatureValue

test('flag on', () => {
  const userContext = { attributes: { id: 'user-1' } };
  expect(gbClient.isOn('show-new-ui', userContext)).toBe(true);
});

test('typed feature value', () => {
  const userContext = { attributes: { id: 'user-1' } };
  const color = gbClient.getFeatureValue('button-color', 'blue', userContext);
  expect(['blue', 'red', 'green']).toContain(color);
});

Scoped instance per test

test('user-specific evaluation', () => {
  const instance = gbClient.createScopedInstance({
    attributes: { id: 'user-1', plan: 'premium' },
  });
  expect(instance.isOn('premium-feature')).toBe(true);
});

Avoids passing context everywhere.

Inline experiments

test('inline experiment returns one variant', () => {
  const userContext = { attributes: { id: 'user-1' } };
  const { value } = gbClient.runInlineExperiment({
    key: 'my-experiment',
    variations: ['red', 'blue', 'green'],
    coverage: 1.0,
    weights: [0.33, 0.34, 0.33],
  }, userContext);
  expect(['red', 'blue', 'green']).toContain(value);
});

Tracking-callback assertions

test('experiment tracking fires', () => {
  const tracked: any[] = [];
  const client = new GrowthBookClient({
    trackingCallback: (exp, result, ctx) => {
      tracked.push({ key: exp.key, variation: result.key });
    },
  }).initSync({ payload: {} });

  client.runInlineExperiment(
    { key: 'exp-x', variations: [0, 1] },
    { attributes: { id: 'user-1' } }
  );

  expect(tracked).toHaveLength(1);
  expect(tracked[0].key).toBe('exp-x');
});

Feature-usage callback

const evaluated: any[] = [];

test('feature usage logged', () => {
  const userContext = {
    attributes: { id: 'user-1' },
    onFeatureUsage: (key: string, result: any) => {
      evaluated.push({ key, value: result.value });
    },
  };

  gbClient.evalFeature('feature-x', userContext);
  expect(evaluated).toContainEqual(expect.objectContaining({ key: 'feature-x' }));
});

TypeScript-strict feature contract

interface AppFeatures {
  'button-color': string;
  'font-size': number;
  'newForm': boolean;
}

const gbClient = new GrowthBookClient<AppFeatures>({}).initSync({ payload: {} });

const color = gbClient.getFeatureValue('button-color', 'blue', ctx);  // typed
// gbClient.isOn('buton-color', ctx);  // typo → compile error

CI integration

Fully offline; no GrowthBook key needed:

jobs:
  growthbook-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-node@v4
      - run: npm ci && npm test

Anti-patterns

Anti-patternWhy it failsFix
Live apiHost in testsNetwork requests; flakyinitSync({ payload })
Skipping tracking-callback assertionExposure-event regressions silentPer-test callback + assert
Sharing scopedInstance across testsCross-test statePer-test create
TypeScript any-typed featuresLose compile-time safetyGeneric AppFeatures
coverage: 0.1 in test without large NNot enough samples to see all variationsUse coverage: 1.0 for deterministic tests
Missing user.id in contextBucketing degenerateAlways pass attributes.id
Tests assume default weights is 50/50Default behaviour driftsExplicit weights

Limitations

  • Payload is point-in-time. Drift from GrowthBook UI invisible.
  • No targeting-rule-test helper beyond writing conditions. Complex targeting tests need many fixture variants.
  • Tracking-callback is synchronous. Async exposure logging needs a separate test.
  • TypeScript strict-mode required for the typed feature contract.

Sources

SKILL.md

tile.json