Chapter 8 of 9

Testing With Feature Flags

How to keep your test suite sane when every feature can be on, off, or half rolled out to a subset of users.

12 min read

The problem flags create for testing

Feature flags multiply the number of states your software can be in. One flag doubles the possible behaviors. Ten independent flags describe over a thousand combinations. You cannot test them all, and pretending otherwise leads to a suite that is slow, brittle, and still misses the combination that breaks in production. The goal of this chapter is a testing strategy that scales with your flag count instead of exploding with it.

The core insight is that not all flags deserve the same testing effort, and not all combinations are real. Most flags are independent, short lived, and only meaningfully tested in the state they will actually ship in. A small number gate risky behavior and deserve both branches tested. Sort your flags into those buckets and the problem shrinks fast.

Make flag state injectable, not ambient

The first rule of testing flagged code is that a test must control the flags. If your code reads flag values from a global client that talks to the network, your tests are at the mercy of whatever is configured in the dashboard right now. That is the definition of flaky.

The fix is dependency injection. Pass the flag client, or a small interface in front of it, into the code under test. In production you wire in the real SDK. In tests you wire in a fake that returns exactly the values the test declares.

// Production wiring
const flags = createLaunchDarklyClient(env.SDK_KEY);

// Test wiring
const flags = createFakeFlags({ "new-checkout": true });

const result = checkout(cart, flags);

Every major SDK supports this. LaunchDarkly ships a test data source, Statsig supports local overrides, and open source tools let you point the SDK at a static file. Whatever the mechanism, the outcome is the same: the test, not the environment, owns the flag values.

Test the branches that matter, not the matrix

Once flags are injectable, decide what to actually test. Use a simple triage.

Release flags that gate a new feature during rollout get both branches tested: the old behavior stays correct while the flag is off, and the new behavior is correct while it is on. This is the whole point of the flag, so both paths must work.

Ops and kill switch flags get their off, or safe, state tested most carefully, because that is the state you will slam into during an incident. If your kill switch has never been exercised in a test, you do not have a kill switch, you have a hope.

Experiment flags need each variation to be individually correct, but the framework, not your unit tests, handles assignment. We come back to experiments below.

Stale flags that are fully rolled out need only their production state tested, because they are on their way to deletion.

You do not test the cartesian product. You test each flag’s relevant branches in isolation, plus a tiny number of known interacting combinations where two flags genuinely touch the same code path. If two flags never interact, testing them together buys nothing.

Integration and end to end tests

Unit tests with injected fakes cover logic, but you also want confidence that the flag actually flips real behavior end to end. For this, run a small number of end to end tests with the flag forced into a known state through an environment variable, a query parameter, or a dedicated test environment in your provider.

A clean pattern is a per environment flag configuration. Your staging environment can force the release flag on so the entire staging suite exercises the new path, while production stays at a 1 percent rollout. Most platforms model environments as first class, so the same flag key carries different rules per environment without code changes. This pairs naturally with the rollout strategies you use to ship, and with canary releases, where the canary population is itself a test of the on state against real traffic.

Testing in production is a feature, not a sin

Flags let you do something traditional testing cannot: validate a change against real users without exposing everyone. A 1 percent rollout is a live test with an instant undo. Watching error rates and latency for that 1 percent tells you things no staging environment can, because staging never has your real data, real load, or real edge cases.

This is where the events pipeline from the architecture chapter earns its keep. Each exposure and each downstream metric flows back so you can compare the flagged cohort against everyone else. If the new path shows a spike in errors, you flip the flag off and the experiment ends in seconds. Treat progressive rollout as your final, highest fidelity test stage, not as a replacement for the earlier ones.

Experiments are tests with statistics attached

An A/B test is a controlled experiment where the flag decides which variation each user sees and a stats engine decides whether the difference is real. The testing discipline shifts from correctness to significance. Tools like Statsig and GrowthBook pair the flag with an analysis layer that computes confidence intervals and guards against calling a winner too early. Your job in code is narrow: make each variation correct and emit a clean exposure event. The platform handles the math. Reading a result before it reaches significance is the most common and most expensive testing mistake teams make here.

Clean up, or the suite rots

Every flag you add is a testing liability until you remove it. A flag left at 100 percent for months still forces its dead branch to compile, still shows up in combination reasoning, and still confuses the next engineer. Budget flag removal as part of finishing a feature, delete the tests that only covered the losing branch, and keep the suite honest. Our guide on cleaning up feature flags covers how to find and retire the stragglers.

Key takeaways

  • Inject flag state into the code under test so each test pins its own values. Ambient flags read from the network are the root of flag flakiness.
  • Triage flags: test both branches of release and kill switch flags, and only the production state of stale ones. Never test the full combinatorial matrix.
  • Use per environment configuration for end to end tests, and treat a small percentage rollout as your highest fidelity, real traffic test stage.
  • Experiments are tests with statistics: make each variation correct, emit clean exposures, and let the platform decide significance.
  • Delete flags and their dead branch tests as soon as a feature is fully shipped.

You now know how to design a flag system and how to test it. The last chapter, Choosing a Feature Flag Platform, ties it together and helps you pick the tool that fits how you actually build.

Frequently Asked Questions

Should I test both the on and off state of every flag?

Test both states for flags that gate risky or user visible behavior, and for the release flag itself during its rollout window. For the long tail of stable flags that will soon be removed, testing the intended production state is enough. Testing every combination of every flag is neither possible nor useful.

How do I stop feature flags from making my tests flaky?

Never let a test read live flag values from the provider. Inject a test double or a local overrides file so each test pins the exact flag states it needs. Flakiness almost always comes from tests sharing a mutable flag source that something else can change mid run.

When is it safe to delete the flag and its tests?

Once the feature is fully rolled out, the flag has been at 100 percent for long enough to trust, and no rollback is planned, remove the flag, collapse the code to the winning branch, and delete the tests that only existed to cover the losing branch.

Continue Learning

Newsletter

Get the Feature Flags Newsletter

Platform benchmarks, real pricing data and progressive delivery practice. No spam.

Free. Unsubscribe any time. See our privacy policy.