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

  1. 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.
  2. Force a state where only one matters. Use withFlag for a single test that needs the flag on (or off).
  3. Pin a flag for a whole file with setFlagOverride, and clear it afterwards.
  4. 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.
  • explain reports "override" as the reason when an override decided the answer (Go).
  • To test the real rules instead, load a ruleset directly with setRules (Node) or SetRules (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.

Sign up