Umbrella for experimentation-SDK test harnesses: the shared offline-datafile / hermetic-init pattern (commit a point-in-time flag/experiment config fixture, initialize the SDK with no network, pin arms per test, assert assignment integrity), with per-vendor references for Statsig (localMode + overrideGate), Optimizely (datafile + forced decisions), Split.io / Harness FME (localhost mode + features map or YAML fixture), Amplitude Experiment (local evaluation + bootstrap), and VWO (settings file + deterministic bucketing). Use when writing tests for application code instrumented with any of these five experimentation SDKs; for experiment DESIGN gates use ab-test-validity-checklist, and to read results use experiment-results-interpreter.
76
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Every major experimentation SDK ships the same hermetic-test mechanism under a different name: a point-in-time config fixture (datafile, settings file, flag payload, features map) that the SDK evaluates locally, so tests make zero network calls, pollute no production analytics, and stay deterministic. The vendor-specific mechanics differ only in how the fixture is loaded and how an arm is pinned.
| SDK | Offline mechanism | Arm pinning | Reference |
|---|---|---|---|
| Statsig | localMode: true | overrideGate / overrideConfig | references/statsig.md |
| Optimizely | JSON datafile fixture | set_forced_decision | references/optimizely.md (+ optimizely-recipes.md) |
| Split.io / Harness FME | authorizationKey: 'localhost' + features map / YAML | Per-key fixture entry (no override API) | references/split-io.md (+ split-io-example.md) |
| Amplitude Experiment | LocalEvaluationClient + bootstrap | Fixture edit or evaluateV2 mock | references/amplitude.md |
| VWO | Settings file + is_development_mode | Deterministic bucketing on user ID | references/vwo.md |
ab-test-validity-checklist Step 3.Regardless of vendor, the suite has the same five steps:
tests/fixtures/ and
refreshed deliberately (drift between fixture and prod config is
invisible otherwise).localMode, datafile string, 'localhost' key, bootstrap,
is_development_mode).Plus two integrity tests every suite should carry:
The team ships a new_checkout_flow flag with a treatment_a variation
and needs a deterministic test that a premium-plan user is routed into the
treatment:
import json
from optimizely import optimizely
# Step 1-2: committed fixture, offline init - no SDK key, no network
with open("tests/fixtures/optimizely-datafile.json") as f:
client = optimizely.Optimizely(f.read())
def test_premium_user_in_treatment():
# Step 3: context carries the attributes targeting needs
user = client.create_user_context("user-1", {"plan": "premium"})
decision = user.decide("new_checkout_flow")
# Step 4: assert on enabled + variation_key, not IDs
assert decision.enabled is True
assert decision.variation_key == "treatment_a"
def test_assignment_deterministic():
user = client.create_user_context("user-1")
d1 = user.decide("new_checkout_flow")
d2 = user.decide("new_checkout_flow")
assert d1.variation_key == d2.variation_keyThe fixture drives the whole decision; the same shape translates to each vendor via its reference above.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Live API key in tests | Production analytics polluted; rate limits; flakes | The vendor's offline switch |
| Fixture not version-controlled | Tests flake when prod config changes | Commit; refresh deliberately |
| Overrides / forced decisions leak across tests | Cross-test pollution | Per-test context + cleanup |
| Asserting on internal config / variation IDs | IDs change per environment | Assert keys and values |
| Skipping client shutdown / destroy | Event-flush timers and handles leak | Teardown in afterAll |
| Trusting one user ID to cover both arms | May bucket into one arm only | Distribution test across many IDs |
ab-test-validity-checklist.experiment-results-interpreter.sample-ratio-mismatch-detector agent.