Feature flag testing: verify a flag without redeploying
Feature flag testing does not require a deploy. dif qa --user <id> --force <flag>=<variant> traces exactly which variant a user would get and why, then mints a preview link that forces the same variant in a browser. No code ships, and the preview fires no exposure.
Most teams test a flag by merging a branch, waiting for the build, and hoping the 10% rollout happens to catch the account they are using. That rarely tells you anything on a schedule you control. Testing a feature flag should be a command you run against the files already in your repo, not a deploy you wait on.
This post covers tracing an assignment, forcing a variant, reading the preview link dif mints, and the one thing this kind of testing does not replace.
Key Takeaways
dif qa --user <id> --attr key=valuetraces exactly which variant a user gets and why, computed locally against the files in your repo.--force <flag>=<variant>bypasses audience targeting and exclusion groups, and mints a preview link (?_dif=id=variant) that forces the same variant in a browser.- A forced preview fires no exposure event, so QA traffic never shows up in your experiment results.
- The force lives in a session cookie that clears when the tab closes;
?_dif=offclears it on demand, and a small on-page badge shows whoever is previewing it that the state is forced.dif qaalways exits 0. It is a debugging tool, not the CI gate:dif validateowns that job.
Test a feature flag without shipping a new build
A feature flag already decides, at render time, which variant a given user sees. Testing one means asking that same decision on demand, for a user you choose, without waiting for a deploy to put you in the rollout by luck.
dif qa answers that question by running the same bucketing math the SDK runs in production, against the flag files already in your repo:
dif qa --user u_8131 --attr device_type=mobile
trace u_8131:
• checkout-cta-v2 → variant_a (bucket 4021)
• pricing-headline → value (bucket 71)
The trace is the exact bucketing outcome for that user ID against every active flag and experiment, computed in a few milliseconds on your machine, against files that are already in your repo.
Trace a user’s assignment, then force one
Tracing tells you what happens today. Testing usually means asking what would happen if a user landed in the other variant, so you can check it before 1% of real traffic does.
--force overrides the computed bucket for one or more flags:
dif qa --user u_8131 --force checkout-cta-v2=variant_a --force pricing-headline=value
trace u_8131:
• checkout-cta-v2 → variant_a (forced)
• pricing-headline → value (forced)
preview: http://localhost:3000?_dif=checkout-cta-v2%3Dvariant_a,pricing-headline%3Dvalue
open in the app to force these variants — fires no exposure; ?_dif=off clears
A forced row bypasses audience targeting and exclusion groups entirely. That matters for the case a real bucket can’t test on its own: you want to see the variant your rollout audience doesn’t include yet, or the losing side of an exclusion group, before you ship it to anyone.
Read the preview link dif mints
The preview: line is a real URL, not a placeholder. dif qa sorts the forced pairs alphabetically by flag id and percent-encodes the result into a _dif query parameter, per RFC 3986. Pass --preview-url to point it at a staging domain instead of localhost:3000:
dif qa --user u_8131 --force new-checkout=on --preview-url https://staging.example.com
preview: https://staging.example.com?_dif=new-checkout%3Don
Open that link in a browser and the SDK reads the _dif param, writes the forced variants to a session cookie (no max-age, so it clears when the tab closes), and strips the param from the address bar. Every dif() call for that flag now returns the forced variant for the rest of the session. ?_dif=off or ?_dif=clear removes the force and reloads to the real assignment.
See the forced state, not just trust it
A forced preview also mounts a small fixed badge in the bottom-right corner of the page: each forced id → variant pair, and a “clear ✕” button. Anyone looking at the page, not just the person who ran the command, can tell they are looking at a forced state and not the real rollout.
A PM gets a link to paste into Slack and a badge that confirms what they’re seeing. An engineer gets the same trace in --json for a test script:
dif qa --user u_8131 --force checkout-cta-v2=variant_a --json
{
"user_id": "u_8131",
"assignments": [
{ "experiment": "checkout-cta-v2", "surface": "checkout", "outcome": "forced", "variant": "variant_a" }
],
"preview_url": "http://localhost:3000?_dif=checkout-cta-v2%3Dvariant_a"
}
A CI smoke test, an end-to-end suite, or a screenshot script can parse that output directly instead of scraping a browser for which variant rendered.
Test an audience-gated flag with --attr
Forcing answers “what does this variant look like.” Testing a targeting rule answers a different question: “would this user even qualify.” --attr sets an audience attribute for the trace, parsed as YAML, so true, 42, and US all work as values:
dif qa --user u_55 --attr country=DE --attr returning_visitor=true
trace u_55:
• eu-pricing-test ↛ audience miss
That trace says the attribute set does not satisfy the flag’s audience rule, with no forcing applied. Add the attribute the rule actually checks for, rerun, and watch the outcome flip from a miss to a real bucket assignment. That is feature flag testing without touching application code: changing the inputs to the same function the SDK calls in production, not pasting the flag check into a browser console.
What dif qa testing does not replace
dif qa always exits 0. It is a debugging tool, not a validator, and it is not the same test as the one that belongs in CI. Validating a flag file in a pull request catches a bad weight sum or a collision before merge. dif qa answers “what does a real user see,” a question a schema check cannot answer.
A forced preview fires no exposure event by design, which is exactly the point for a human clicking through a variant, but it means forcing a variant is the wrong tool for testing your exposure-to-analytics pipeline end to end. For that you need a real bucketed user and a real exposure fire, not a forced one. And without --user, dif generates a new synthetic ID on every run: good for eyeballing bucket spread across many users, bad for rerunning the exact same test twice. Pass --user explicitly whenever you need a reproducible trace.
FAQ
How do I test a feature flag without deploying? Run dif qa --user <id> --force <flag>=<variant> against your repo. It computes the real bucketing outcome locally and, when you force a variant, prints a preview link that forces the same variant in a browser. No build or deploy required.
Does forcing a variant with dif qa count as a real exposure? No. A forced preview fires no exposure event, by design, so clicking through a variant to check how it looks never shows up in your experiment data.
Can I test a flag for a specific user or audience attribute? Yes. --user <id> traces a specific user ID, and --attr key=value sets an audience attribute (parsed as YAML) to test whether a targeting rule matches, independent of forcing a variant.
What’s the difference between dif qa and dif validate? dif validate is the CI gate: it checks flag files for schema errors and always exits non-zero on a real problem. dif qa always exits 0. It traces and forces assignment for a human to look at; it does not fail a build.
Can I share a preview link with someone who isn’t an engineer? Yes. The link is a plain URL (?_dif=flag%3Dvariant). Opening it mounts a small badge showing exactly which flags are forced, so a PM or designer can preview a variant and know they’re looking at a forced state, not the live rollout.
How do I clear a forced variant? Open the same URL with ?_dif=off (or clear), or click “clear ✕” on the on-page badge. Either removes the session cookie and reloads to the real assignment.
Getting started
Feature flag testing in dif is a CLI command, not a deploy. dif qa --user <id> --attr key=value traces the real assignment for any user against the flags already in your repo. Add --force <flag>=<variant> to override one and get a preview link that forces it in a browser, fires no exposure, and shows a clearable badge to whoever opens it. It is a debugging tool, not the CI gate: pair it with dif validate for schema checks, and pass --user explicitly whenever the test needs to be reproducible.
Install the CLI and try it against a real flag:
npm install -g @dif.sh/cli
dif init
Then see the CLI reference for the rest of qa’s flags, or read how feature flags live in your repo in the first place.