Testing both sides of a flag
Test the code with the flag on and with it off. Flagger.gg's SDKs include helpers for this: bothFlagStates runs a test twice, withFlag forces a state for one test, and setFlagOverride pins one. Go has the same in flaggertest, and a nil client is valid with everything off.
Updated
Steps
- Run the test in both states. Wrap any test that touches flagged code in
bothFlagStates, so both paths stay covered for as long as the flag exists. - Force a state where only one matters. Use
withFlagfor a single test that needs the flag on (or off). - Pin a flag for a whole file with
setFlagOverride, and clear it afterwards. - Delete the helpers with the flag. When the flag is removed, the off branch of each test goes with it.
Node / TypeScript
import { test } from "node:test";
import assert from "node:assert/strict";
import { flagger } from "../src/flags.js";
test("checkout total", () =>
flagger.bothFlagStates("checkout_v2", async (on) => {
const total = await checkoutTotal(cart);
assert.equal(total, on ? 95 : 100);
}));
test("v2 shows the discount", () =>
flagger.withFlag("checkout_v2", true, async () => {
assert.match(await renderCheckout(cart), /discount/);
}));
flagger.setFlagOverride("checkout_v2", true); // pin it
flagger.setFlagOverride("checkout_v2", undefined); // clear it
Go
import (
"testing"
"flagger.gg/go/flaggertest"
)
func TestCheckoutTotal(t *testing.T) {
flaggertest.BothFlagStates(t, client, "checkout_v2", func(t *testing.T, on bool) {
want := 100
if on {
want = 95
}
if got := checkoutTotal(cart); got != want {
t.Fatalf("got %d, want %d", got, want)
}
})
}
func TestDiscountShown(t *testing.T) {
flaggertest.WithFlag(t, client, "checkout_v2", true, func(t *testing.T) {
// runs with checkout_v2 forced on
})
flaggertest.SetFlagOverride(t, client, "other_flag", false) // until t.Cleanup
}
A nil *flagger.Client is valid, and every flag on it is off, so tests and tools that never call New work without a key.
How overrides behave
- Overrides win in every SDK mode, including shadow and local.
- They're per client and in memory: they never reach Flagger.gg or change a flag for anyone else.
explainreports"override"as the reason when an override decided the answer (Go).- To test the real rules instead, load a ruleset directly with
setRules(Node) orSetRules(Go).
FAQ
How do I unit test code behind a feature flag?
Run the test with the flag on and with it off. In Flagger.gg's SDKs, bothFlagStates does both in one call.
Do tests need a Flagger.gg key or network access?
No. With an empty SDK key the SDKs make no network calls, overrides are in memory, and in Go a nil client is valid with every flag off.
Should I test every combination of flags?
Usually not. Test each flag's two states where its code runs; combinations only matter where two flags touch the same code.
Ship your next change behind a flag.
Sign up, add an SDK key to your app and give your agent a token.