CtrlK
BlogDocsLog inGet started
Tessl Logo

testland/openfeature-sdk-testing

Wraps OpenFeature (CNCF vendor-neutral SDK abstraction) testing patterns: the InMemoryProvider for hermetic tests without network calls, provider registration via OpenFeature.setProvider, the getBooleanValue/getBooleanDetails evaluation API with EvaluationDetails (value, variant, reason, errorCode), hooks for evaluation side-effects, and evaluation context for targeting-rule tests. Covers TypeScript, Java, and Python SDKs, plus per-vendor hermetic-bootstrap references for Unleash (bootstrap toggles), Flagsmith (offline LocalFileHandler), and GrowthBook (initSync payload). Use when writing tests for code that resolves feature flags through the OpenFeature SDK or the Unleash / Flagsmith / GrowthBook native SDKs; LaunchDarkly has its own skill (launchdarkly-testing).

72

Quality

90%

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