Shipeasy
Flags & ExperimentsCase studies

Deterministic flags in CI and unit tests

Use forTesting() + overrides for unit tests and a captured snapshot for integration and CI — flag-gated code that never touches the network.

Production readyOn this page · 4 min readUpdated · June 19, 2026Works with · @shipeasy/sdk · server + client

A test that reads a flag over the network is flaky by construction: it depends on what the dashboard says right now, it fails in CI behind a firewall, and a teammate flipping a gate breaks an unrelated build. You want flag-gated code to be testable the way the rest of your code is — deterministic, offline, and explicit about what each case sets.

The SDK gives you two levels: forTesting() + overrides for unit tests, and a snapshot for integration and CI.

Unit tests — forTesting() + overrides

forTesting() builds a no-network client that's already "ready": init() / identify() / track() are no-ops, telemetry is off, and no SDK key is required. You seed each case explicitly:

import { Engine } from "@shipeasy/sdk/server";

const engine = Engine.forTesting();

engine.overrideFlag("new_checkout", true);
engine.overrideConfig("upload_limits", { max_uploads: 50 });
engine.overrideExperiment("hero_cta", "treatment", { primary_label: "Buy now" });

engine.getFlag("new_checkout", { user_id: "u1" }); // true
engine.getConfig("upload_limits"); // { max_uploads: 50 }
engine.track("u1", "purchase"); // no-op — never hits the network
engine.clearOverrides(); // reset between cases

The browser is identical via Engine.forTesting() from @shipeasy/sdk/client.

Integration / CI — a captured snapshot

When you want the real rule set — actual targeting, actual bucketing — without a live dependency, evaluate against a snapshot. A snapshot is just the two SDK wire bodies on disk:

import { Engine } from "@shipeasy/sdk/server";

// from a file (Node — reads with node:fs)
const engine = Engine.fromFile("./snapshot.json");

// or from an object you already hold (works anywhere)
const engine = Engine.fromSnapshot({ flags, experiments });

engine.getFlag("new_checkout", { user_id: "u1" }); // real eval, no network
Two tools, they compose

Use forTesting() + override* to seed individual values; use a snapshot when you want the real rules evaluated offline. override* applies on top of a snapshot too, so you can take production rules and still force one specific case.

Rollout & measurement plan

Default to forTesting() for unit tests

Seed exactly the flags the unit under test reads. Each test states its own world.

Capture a snapshot for the integration suite

Save the bodies of GET /sdk/flags and GET /sdk/experiments; commit it so CI gets real evaluation with no network.

Refresh the snapshot on purpose

A stale snapshot is a known, reviewable diff — re-capture when the rules you depend on change.

Was this page helpful?
✎ Edit this page

On this page