QA How-To
Test Feature Flag Percentage Rollouts (2026)
Learn to test feature flag percentage rollouts with deterministic hashing, sticky cohorts, boundary checks, distribution tests, telemetry, and rollback proof.
20 min read | 3,308 words
TL;DR
Test percentage rollouts at three layers: deterministic assignment, application behavior for control and treatment, and observed production exposure. Check boundary percentages, sticky identity, monotonic expansion, targeting precedence, cohort distribution, telemetry integrity, and rollback before increasing traffic.
Key Takeaways
- Test assignment invariants separately from product behavior and release health.
- Use a stable identity, flag key, and salt so a user does not switch cohorts between requests.
- Prove zero and 100 percent boundaries before running statistical distribution checks.
- Verify that increasing a rollout preserves the earlier cohort when configuration inputs stay fixed.
- Compare configured allocation with unique eligible identities, not raw page views or all traffic.
- Keep exclusions, exact includes, the global kill switch, and percentage rules in an explicit precedence order.
- Exercise rollback while the old path can still read data produced by the new path.
To test feature flag percentage rollouts, prove that the same eligible identity receives a stable decision, the selected share is reasonable across a large deterministic fixture set, and both treatment and control paths behave correctly. Then compare configuration with real evaluation telemetry and rehearse a return to the stable path before exposure grows.
A percentage value by itself is not evidence. A dashboard can say 25 percent while the application sends the wrong identity, evaluates in the wrong environment, repeatedly reassigns anonymous visitors, or records only successful treatment events. This tutorial builds a small reference evaluator and a focused test suite that exposes those failures without depending on a vendor's private bucketing algorithm.
If your company uses a managed flag service, keep the invariants and telemetry checks from this guide, but call the provider's documented SDK through your own adapter. Do not copy this hash function and expect its exact buckets to match a vendor implementation. For production targeting controls and safe accounts, pair this tutorial with testing feature flags in production.
What You Will Build
You will create a dependency-free Node.js project that models a gradual rollout for checkout_redesign. The finished suite will:
- map an immutable identity into one of 10,000 stable buckets;
- support percentages with two decimal places, including 0 and 100;
- enforce global disable, exclusion, exact inclusion, and percentage precedence;
- test sticky assignment, flag isolation, distribution, expansion, and salt changes;
- summarize unique-identity exposure events and detect conflicting decisions;
- run the entire contract in CI with Node's built-in test runner.
The reference uses SHA-256 from node:crypto. Cryptographic strength is not the release goal, but a portable digest avoids process-specific seeds and makes fixture results reproducible across machines. The suite treats the assignment algorithm as versioned behavior. Changing the key format, identity, hash, bucket count, or salt becomes a deliberate cohort migration instead of an invisible refactor.
You are testing three different questions, each with a separate oracle:
| Layer | Main question | Reliable oracle | Misleading shortcut |
|---|---|---|---|
| Assignment | Who receives treatment? | Evaluated variation, reason, bucket, and stable identity | Visible UI alone |
| Behavior | Does each path work? | API, UI, data, and side-effect assertions for both variations | Treatment happy path only |
| Release | Is expansion safe? | Eligible exposure, cohort health, guardrails, and rollback result | Configured percentage alone |
Prerequisites
Use Node.js 24.18.0 LTS and the bundled npm 11.16.0. The code relies only on stable node:crypto, node:test, and node:assert/strict APIs. Git 2.50.1 is useful if you want to commit the sample, but Git is not required to run it. Commands below use a POSIX shell; PowerShell users can create the same directories and files with equivalent commands.
Check the exact runtime first:
node --version
npm --version
Expected output is v24.18.0 followed by 11.16.0. A later Node 24 patch should also run the sample, but pin one version locally and in CI so a runtime update cannot be confused with an allocation change. You do not need a feature flag account, browser, database, or secret. When you later connect a provider adapter, store its server credential outside client code and follow CI test secret management.
Verification command: run node -e "console.log(process.versions.node)". It should print 24.18.0 before you create assignment fixtures.
Step 1: Define What Test Feature Flag Percentage Rollouts Must Prove
Start with a written contract. The percentage applies to eligible identities after exclusions and exact targeting, not to every request arriving at the application. One user making 50 requests should count as one assigned identity when you validate cohort allocation. Raw request share is a different operational metric and can be skewed by highly active users.
Use an immutable, non-sensitive key such as an internal account UUID. Email addresses can change, device IDs can reset, and random values generated per request destroy stickiness. Decide how anonymous identity merges into an authenticated account before launch. A user who moves from an anonymous cookie to a database ID may legitimately change buckets unless the system preserves the earlier assignment. Write that transition into the product contract instead of discovering it from a support ticket.
Define rule order explicitly: global disabled -> excluded identity -> exact include -> percentage bucket. In this design, the kill switch overrides every other rule, exclusions beat includes, and an exact include bypasses the percentage threshold. A different order can be valid, but tests and release documentation must agree. Also pin allocation-v1 as the salt. The salt gives you a controlled way to create a fresh sample later, yet changing it during an active rollout reshuffles users.
Turn the contract into a review matrix before coding. Give each row an identity class, expected variation, expected reason, observable product outcome, and rollback outcome. Include an excluded tenant, an internal treatment account, ordinary identities on both sides of the threshold, an anonymous visitor who later signs in, and a request with missing identity. Name the owner of the identity schema and allocation version. Also define whether evaluation occurs per request, per session, or per server action. A cached decision can be valid, but its cache key and expiry must still respect the required emergency disablement time.
Create the project and its repeatable test command:
mkdir flag-rollout-tests
cd flag-rollout-tests
npm init -y
npm pkg set type=module
npm pkg set scripts.test="node --test"
npm install --package-lock-only
mkdir -p src test
The lockfile has no external packages, but it makes npm ci deterministic in CI. Keep flag-administration credentials out of this project because assignment tests need read-only decisions, not the power to change production configuration.
Verification command: run npm pkg get type scripts.test. The two values should be "module" and "node --test".
Step 2: Implement a Deterministic Percentage Evaluator
Create src/rollout.js. The evaluator joins salt, flag key, and identity with a null separator, hashes the result, reads the first unsigned 32-bit value, and scales it to 10,000 buckets. A 25 percent rollout enables buckets 0 through 2499. Using < threshold gives exact boundary semantics: 0 enables nobody and 100 enables every valid identity.
import { createHash } from 'node:crypto';
const BASIS_POINTS = 10_000;
const UINT32_RANGE = 0x1_0000_0000;
export function toBasisPoints(percentage) {
if (!Number.isFinite(percentage) || percentage < 0 || percentage > 100) {
throw new RangeError('percentage must be between 0 and 100');
}
return Math.round(percentage * 100);
}
export function bucketFor(flagKey, identity, salt = 'allocation-v1') {
if (typeof flagKey !== 'string' || flagKey.length === 0) {
throw new TypeError('flagKey must be a non-empty string');
}
if (typeof identity !== 'string' || identity.length === 0) {
throw new TypeError('identity must be a non-empty string');
}
const digest = createHash('sha256')
.update(`${salt}\u0000${flagKey}\u0000${identity}`)
.digest();
const ratio = digest.readUInt32BE(0) / UINT32_RANGE;
return Math.floor(ratio * BASIS_POINTS);
}
export function isInPercentageRollout(flagKey, identity, percentage, salt) {
return bucketFor(flagKey, identity, salt) < toBasisPoints(percentage);
}
export function evaluateFlag(rule, identity) {
const threshold = toBasisPoints(rule.percentage);
const includeIds = rule.includeIds ?? [];
const excludeIds = rule.excludeIds ?? [];
if (!rule.enabled) {
return { enabled: false, reason: 'disabled', bucket: null };
}
if (excludeIds.includes(identity)) {
return { enabled: false, reason: 'excluded', bucket: null };
}
if (includeIds.includes(identity)) {
return { enabled: true, reason: 'included', bucket: null };
}
const bucket = bucketFor(rule.key, identity, rule.salt);
return {
enabled: bucket < threshold,
reason: 'percentage',
bucket,
};
}
Do not trim or lowercase identity inside the evaluator. Silent normalization can merge distinct IDs or make the test implementation disagree with the production adapter. Canonicalize once at the identity boundary, then pass the exact stored value to every evaluator. The bucket is safe diagnostic metadata only if the identity itself is not logged beside it.
Verification command: run node --input-type=module -e "import { bucketFor } from './src/rollout.js'; console.log(bucketFor('checkout_redesign', 'qa-user-17'))". Expected output is 8041.
Step 3: Lock Boundary Values and a Known Assignment
A statistical test cannot replace exact boundary checks. First prove the evaluator rejects invalid percentages, preserves a known bucket, disables every identity at 0, and enables every identity at 100. The known fixture catches changes to separators, input order, character encoding, digest, and integer conversion. Review any fixture update as a cohort migration because real users may switch branches.
Add a negative identity contract beside the happy fixtures. Empty input should fail before hashing, while a syntactically valid but unknown account may still receive a percentage decision if eligibility is checked elsewhere. Keep those responsibilities distinct. The rollout utility should not silently invent an anonymous key when identity is absent. In an application adapter, decide whether missing context returns the stable variation, raises an operational error, or skips evaluation, then assert that documented behavior. This prevents a login or data-loading fault from accidentally widening treatment.
Create test/rollout.contract.test.js:
import test from 'node:test';
import assert from 'node:assert/strict';
import {
bucketFor,
isInPercentageRollout,
toBasisPoints,
} from '../src/rollout.js';
test('keeps the versioned assignment fixture stable', () => {
assert.equal(bucketFor('checkout_redesign', 'qa-user-17'), 8041);
});
test('handles zero and full rollout boundaries', () => {
assert.equal(
isInPercentageRollout('checkout_redesign', 'qa-user-17', 0),
false,
);
assert.equal(
isInPercentageRollout('checkout_redesign', 'qa-user-17', 100),
true,
);
});
test('accepts basis-point precision and rejects invalid percentages', () => {
assert.equal(toBasisPoints(12.34), 1234);
assert.throws(() => toBasisPoints(-0.01), RangeError);
assert.throws(() => toBasisPoints(100.01), RangeError);
assert.throws(() => toBasisPoints(Number.NaN), RangeError);
});
The 12.34 case documents rounding to basis points. If your provider supports only whole percentages or uses a larger bucket space, encode that real contract instead. For a managed service, replace the fixed bucket assertion with a documented evaluation fixture or an adapter-level stub. Never assert an internal vendor bucket that the public SDK does not promise.
Verification command: run node --test test/rollout.contract.test.js. The summary should report three passing tests and zero failures.
Step 4: Test Sticky Identity and Flag Isolation
Sticky assignment means repeated evaluations of the same flag, identity, and allocation version return the same decision across requests, processes, and deploys. It does not mean that a user must receive the same bucket for every flag. Reusing one global bucket creates correlated experiments, so a person selected for one 10 percent rollout is likely selected for every other 10 percent rollout. Including the flag key isolates those samples.
Create test/rollout.stability.test.js:
import test from 'node:test';
import assert from 'node:assert/strict';
import { bucketFor, isInPercentageRollout } from '../src/rollout.js';
test('returns one bucket across repeated evaluations', () => {
const buckets = new Set(
Array.from({ length: 1_000 }, () =>
bucketFor('checkout_redesign', 'account-4f81'),
),
);
assert.equal(buckets.size, 1);
});
test('uses the flag key as part of assignment', () => {
const checkoutBucket = bucketFor('checkout_redesign', 'account-4f81');
const searchBucket = bucketFor('search_ranking_v2', 'account-4f81');
assert.notEqual(checkoutBucket, searchBucket);
});
test('returns the same decision after unrelated evaluations', () => {
const before = isInPercentageRollout(
'checkout_redesign',
'account-4f81',
25,
);
bucketFor('recommendations_v3', 'another-account');
const after = isInPercentageRollout(
'checkout_redesign',
'account-4f81',
25,
);
assert.equal(after, before);
});
Repeat this test against each production service that evaluates the flag. Browser, API, worker, and edge code must send the same canonical identity. If the client uses a device cookie while the server uses an account UUID, a journey can cross variations even though each evaluator is locally deterministic. Record the evaluation reason in sanitized test diagnostics so UI failures are not mistaken for assignment failures.
Verification command: run node --test test/rollout.stability.test.js. Expected output is three passes, with no intermittent result across repeated runs.
Step 5: Measure Distribution Without Writing a Flaky Test
Distribution tests answer whether the hash spreads a representative synthetic identity set plausibly. They do not prove that production will expose the same share, because eligibility rules, identity quality, bot traffic, account activity, and event loss change observed data. Keep this suite deterministic by generating the same 100,000 identities every time.
Choose fixture IDs independently from production identifiers. Sequential synthetic names are suitable here because SHA-256 spreads their input, and they are safe to store in test output. Run a second analysis outside the blocking suite when the real eligible population has meaningful segments such as tenant size, country, or subscription plan. Overall allocation can look correct while a small business-critical segment is absent. Segment checks need minimum counts and privacy rules, so do not turn every dashboard slice into a fragile CI assertion.
For each percentage, calculate the expected count and binomial standard deviation. A four-standard-deviation limit is wide enough for a stable hash fixture while still detecting severe skew, an off-by-one threshold, or accidental decimal handling. Do not assert that exactly 25,000 identities are enabled at 25 percent. Hashing provides a distribution, not a quota.
Create test/rollout.distribution.test.js:
import test from 'node:test';
import assert from 'node:assert/strict';
import { isInPercentageRollout } from '../src/rollout.js';
const SAMPLE_SIZE = 100_000;
const identities = Array.from(
{ length: SAMPLE_SIZE },
(_, index) => `fixture-user-${index}`,
);
for (const percentage of [1, 10, 25, 50, 90]) {
test(`keeps ${percentage}% allocation within four sigma`, () => {
const observed = identities.filter((identity) =>
isInPercentageRollout('checkout_redesign', identity, percentage),
).length;
const probability = percentage / 100;
const expected = SAMPLE_SIZE * probability;
const sigma = Math.sqrt(
SAMPLE_SIZE * probability * (1 - probability),
);
assert.ok(
Math.abs(observed - expected) <= 4 * sigma,
`observed ${observed}; expected about ${expected}`,
);
});
}
The fixed sample should produce roughly 1,057, 10,062, 24,965, 50,031, and 90,036 enabled identities with the reference implementation. Those counts are regression evidence for this exact fixture, not universal benchmarks. A SaaS flag service may use a different hash and still be correct. Test its public promises, then use production telemetry to validate the population your application actually sends.
Verification command: run node --test test/rollout.distribution.test.js. It should report five passes in one deterministic run.
Step 6: Prove Expansion Is Monotonic and Salt Changes Are Intentional
When a rollout moves from 10 to 25 to 50 percent, every identity in the smaller cohort should remain enabled if the flag key, identity, salt, and targeting rules remain unchanged. This monotonic property lets the team add exposure without making earlier treatment users bounce back to control. It also makes a rollback from 50 to 25 predictable: buckets at or above 2500 leave treatment, while the original 25 percent stays.
Create test/rollout.expansion.test.js:
import test from 'node:test';
import assert from 'node:assert/strict';
import { isInPercentageRollout } from '../src/rollout.js';
const identities = Array.from(
{ length: 50_000 },
(_, index) => `expansion-user-${index}`,
);
test('preserves earlier cohorts as percentage increases', () => {
for (const identity of identities) {
const at10 = isInPercentageRollout('checkout_redesign', identity, 10);
const at25 = isInPercentageRollout('checkout_redesign', identity, 25);
const at50 = isInPercentageRollout('checkout_redesign', identity, 50);
if (at10) assert.equal(at25, true, identity);
if (at25) assert.equal(at50, true, identity);
}
});
test('shows that a salt rotation reshuffles membership', () => {
const changed = identities.filter((identity) => {
const v1 = isInPercentageRollout(
'checkout_redesign', identity, 25, 'allocation-v1',
);
const v2 = isInPercentageRollout(
'checkout_redesign', identity, 25, 'allocation-v2',
);
return v1 !== v2;
}).length;
assert.ok(changed > 15_000 && changed < 22_500, `changed ${changed}`);
});
The second assertion is deliberately broad. Its purpose is to catch a salt parameter that is ignored, not certify a particular random sequence. Do not rotate a salt merely because a first cohort looks inconvenient. Rotation invalidates longitudinal comparison and may expose a second set of users to side effects. If a clean sample is required, create an approved allocation version, preserve the old decision in analytics, and explain the migration.
Verification command: run node --test test/rollout.expansion.test.js. Both tests should pass; changing only the percentage must never remove a member during expansion.
Step 7: Test Feature Flag Percentage Rollouts Against Targeting Precedence
Percentage is usually the last rule, not the whole flag. Exact internal accounts may need early treatment, support accounts may need exclusion, and a global switch must stop treatment during an incident. A distribution test that bypasses these rules can pass while production behaves differently. Test the complete decision and its reason.
Write precedence cases from operational risk, not only code branches. A suspended tenant should stay excluded even if support also places it on an include list. An incident disable action should defeat a stale allowlist. A prerequisite flag that protects a backend migration may need to fail closed before this evaluator is reached. If a provider exposes rule identifiers or reason categories, translate them into a small adapter contract instead of leaking vendor-specific response objects throughout tests. That boundary makes configuration drift visible without coupling every product assertion to one service.
Create test/rollout.precedence.test.js:
import test from 'node:test';
import assert from 'node:assert/strict';
import { evaluateFlag, isInPercentageRollout } from '../src/rollout.js';
const baseRule = {
key: 'checkout_redesign',
enabled: true,
percentage: 25,
salt: 'allocation-v1',
includeIds: ['qa-treatment'],
excludeIds: ['regulated-tenant', 'qa-treatment-and-excluded'],
};
test('lets the global switch override an exact include', () => {
const decision = evaluateFlag(
{ ...baseRule, enabled: false },
'qa-treatment',
);
assert.deepEqual(decision, {
enabled: false, reason: 'disabled', bucket: null,
});
});
test('lets exclusion override percentage and inclusion', () => {
const rule = {
...baseRule,
includeIds: [...baseRule.includeIds, 'qa-treatment-and-excluded'],
};
assert.equal(
evaluateFlag(rule, 'qa-treatment-and-excluded').reason,
'excluded',
);
});
test('lets an exact include bypass the percentage', () => {
assert.deepEqual(evaluateFlag(baseRule, 'qa-treatment'), {
enabled: true, reason: 'included', bucket: null,
});
});
test('uses percentage for an ordinary eligible identity', () => {
const identity = 'ordinary-account';
const decision = evaluateFlag(baseRule, identity);
assert.equal(decision.reason, 'percentage');
assert.equal(
decision.enabled,
isInPercentageRollout(
baseRule.key, identity, baseRule.percentage, baseRule.salt,
),
);
});
Add application assertions after the decision contract passes. A control account must see the stable UI, call the stable API behavior, and produce compatible data. A treatment account must exercise the new branch and its failure recovery. When navigation changes, add feature flag route redirect tests; when shared controls change, cover feature flag UI consistency.
Verification command: run node --test test/rollout.precedence.test.js. The result should show four passes, and each failure should name the rule whose order changed.
Step 8: Audit Observed Exposure and Gate the Full Suite
Configuration says what should happen. Evaluation events show what identities actually received. Summarize one event per unique eligible identity, separate exact includes from percentage assignments in a real analytics pipeline, and flag any identity that reports both variations for the same allocation version. Never use treatment-only product events as the denominator because users who fail before emitting the event disappear from the sample.
Design the exposure event before release. Include flag key, variation, reason category, allocation version, application version, environment, event time, and a privacy-safe identity token. Reject or quarantine events with missing environment or version so staging data cannot dilute production counts. Track duplicate evaluations separately from unique assignment because request volume helps capacity analysis but must not inflate cohort size. A conflict should open an investigation even when the aggregate percentage is perfect. It can reveal cross-service identity disagreement, cache lag, or two SDK configurations active during one deployment.
Create src/audit-exposure.js:
export function summarizeExposure(events, flagKey) {
const decisions = new Map();
let conflicts = 0;
for (const event of events) {
if (event.flagKey !== flagKey) continue;
const previous = decisions.get(event.identity);
if (previous !== undefined && previous !== event.enabled) {
conflicts += 1;
}
decisions.set(event.identity, event.enabled);
}
const enabled = [...decisions.values()].filter(Boolean).length;
const eligible = decisions.size;
return {
eligible,
enabled,
exposurePercentage: eligible === 0 ? 0 : (enabled / eligible) * 100,
conflicts,
};
}
Then create test/exposure-audit.test.js:
import test from 'node:test';
import assert from 'node:assert/strict';
import { evaluateFlag } from '../src/rollout.js';
import { summarizeExposure } from '../src/audit-exposure.js';
const rule = {
key: 'checkout_redesign',
enabled: true,
percentage: 25,
salt: 'allocation-v1',
};
const events = Array.from({ length: 10_000 }, (_, index) => {
const identity = `observed-user-${index}`;
const decision = evaluateFlag(rule, identity);
return { flagKey: rule.key, identity, enabled: decision.enabled };
});
test('summarizes unique eligible identities', () => {
const summary = summarizeExposure(events, rule.key);
assert.equal(summary.eligible, 10_000);
assert.equal(summary.conflicts, 0);
assert.ok(Math.abs(summary.exposurePercentage - 25) < 1);
});
test('detects conflicting decisions for one identity', () => {
const first = events[0];
const summary = summarizeExposure(
[...events, { ...first, enabled: !first.enabled }],
rule.key,
);
assert.equal(summary.conflicts, 1);
});
In production, segment this audit by environment and allocation version, then inspect country, plan, device class, and service version only where counts protect privacy and support a decision. Compare technical guardrails and business outcomes with a simultaneous control. The canary testing guide explains how to turn those signals into proceed, hold, or rollback criteria.
Verification command: run npm test. The complete project should report 19 passing tests and zero failures. Run the same command in CI on Node 24.18.0 before anyone changes the hash inputs, rule order, or rollout configuration adapter.
Troubleshooting
Problem: The same user alternates between treatment and control -> Log the sanitized evaluation reason, flag key, allocation version, and identity source at each decision point. Compare browser, API, worker, and edge inputs. Common causes are a random per-request ID, an anonymous-to-authenticated transition, inconsistent Unicode normalization, multiple environments, or one service still using the previous salt. Fix identity mapping before widening exposure.
Problem: The distribution test is consistently far from the target -> Confirm the denominator contains unique eligible identities and that the threshold uses basis points with a strict less-than comparison. Check whether fixtures share a prefix that a weak custom hash handles poorly. With a managed provider, remove assumptions about private bucket numbers and query documented evaluation results through its supported SDK or data export.
Problem: Ten percent users disappear after promotion to 25 percent -> Compare the flag key, salt, canonical identity, targeting rules, and environment between both evaluations. Percentage growth is monotonic only when those inputs remain fixed. Restore the prior allocation version, measure affected identities, and treat any planned salt change as a separate migration.
Problem: The dashboard shows 25 percent but telemetry shows 8 percent -> Establish whether the dashboard percentage applies after prerequisites, segments, and exclusions. Count evaluation events for all eligible identities, including control, and measure ingestion delay or dropped events. A treatment success event is not an exposure event. Validate that each application instance reads the intended flag project and environment.
Problem: Exact QA accounts work while ordinary users never enter treatment -> Inspect rule order and context attributes. An exclusion segment or failed prerequisite may match the whole population before the percentage rule runs. Capture the provider's documented evaluation reason, test one known bucket identity through the real adapter, and verify configuration in the deployed environment rather than a local dashboard tab.
Problem: Turning the flag off hides the UI but new writes continue -> The client and server are not using one authoritative capability decision, or a worker has stale configuration. Keep authorization and irreversible writes server-side, test provider polling or streaming propagation, and verify old readers accept data already created by treatment. A visual rollback without side-effect rollback is incomplete.
Interview Questions and Answers
Interviewers use rollout questions to test whether you can separate deterministic assignment from statistical evidence and product correctness. Be ready to explain why exact counts are the wrong oracle, why a stable identity matters, how rule precedence changes eligibility, and what proves rollback beyond a dashboard toggle. The structured Q&A below also covers vendor-managed bucketing and production telemetry without assuming access to private algorithms.
Best Practices
- Version the allocation contract. Pin the flag key, identity schema, separator, hash or provider configuration, bucket space, and salt.
- Test 0 and 100 percent as exact boundaries. Use statistical tolerance only for intermediate distribution.
- Maintain named control and treatment accounts in addition to synthetic bulk identities. They make application failures reproducible.
- Count unique eligible identities for allocation. Track requests separately when you need capacity or latency analysis.
- Emit variation and evaluation reason for both branches using privacy-safe identifiers and bounded labels.
- Keep the server authoritative for permissions, prices, entitlements, destructive actions, and durable writes.
- Define proceed, hold, and rollback thresholds before the stage begins. Do not reinterpret a surprising metric under deadline pressure.
- Run safe default coverage with feature flag default tests, especially for missing provider data or malformed configuration.
- Preserve backward-compatible data until the rollback window closes, then remove the losing branch and stale targeting rules.
Where To Go Next
Connect the pure contract to your real flag adapter. Keep provider calls behind a narrow function that returns variation, reason, and safe diagnostic metadata. Run the same control and treatment journeys against staging, then use exact internal targeting for the first authorized production check. Do not grant the browser runner administrative access to the flag control plane.
Next, practice staged release decisions with the canary testing workflow and deepen environment-level checks through testing feature flags safely in production. If the feature changes routes or shared components, add the focused redirect and consistency guides linked in Step 7.
Use /practice to rehearse rollout interview scenarios. To turn this project into resume evidence, upload your resume in the QAJobFit dashboard and describe the risk, deterministic oracle, telemetry check, and rollback result rather than listing only the test tool.
Keep a release record beside the suite: configuration snapshot, eligible count, observed treatment count, control and treatment smoke results, telemetry window, decision owner, and rollback duration. This record separates a passing algorithm check from a safe promotion decision. When the flag reaches 100 percent, observe the stabilization window, remove temporary targets, and schedule deletion of the stable branch only after rollback risk closes. Lingering permanent flags keep both code paths and their tests alive.
Conclusion
Reliable percentage rollout testing combines exact contracts with population evidence. Lock the identity and allocation inputs, test boundary and precedence behavior, measure deterministic distribution, prove monotonic expansion, and compare configured percentage with unique-identity evaluation events.
Before moving beyond a small cohort, run both product branches and exercise disablement through the real application path. That sequence turns a percentage slider into an observable, reversible release control.
Interview Questions and Answers
What test cases would you write for a percentage-based feature flag?
I would cover invalid configuration, 0 and 100 percent boundaries, a known assignment fixture, repeated evaluation stability, and independence between flag keys. I would test exclusions, exact includes, prerequisites, and global disable in their documented order. Then I would add deterministic distribution, monotonic expansion, application behavior for both variations, exposure telemetry, and rollback propagation.
How would you test that a 25 percent rollout is correct?
I would not expect exactly one out of every four identities in a small sample. I would evaluate a large fixed identity set, compare the observed count with the binomial expectation using a predefined tolerance, and ensure repeated runs return the same members. In production I would calculate exposure from unique eligible evaluation events and investigate segment composition and telemetry loss.
What causes users to switch feature flag cohorts unexpectedly?
Typical causes are mutable or random identity keys, different identity sources across services, salt rotation, flag-key changes, environment mismatch, and altered targeting precedence. Anonymous users may also switch when they authenticate if the application changes keys. I diagnose the issue by comparing sanitized evaluation inputs, variation, reason, and allocation version at every decision point.
Why is a UI assertion insufficient for feature flag validation?
The UI can be hidden by permissions, API errors, responsive layout, caching, or stale assets even when the flag evaluated true. Conversely, a visible element does not prove the server authorized the new behavior. I verify the decision directly, then test UI, API, durable data, side effects, and telemetry as separate oracles.
How do targeting rules affect a percentage rollout test?
The percentage usually runs after prerequisites, exclusions, and exact targets, so it applies only to the remaining eligible population. My test matrix asserts the documented rule order and evaluation reason, not just the final Boolean. I calculate distribution only for identities that reach the percentage rule.
How would you test rollout expansion from 10 to 50 percent?
With a stable key, identity, salt, and rule set, I assert every member at 10 percent remains enabled at 25 and 50 percent. At each production stage I compare actual evaluations, technical guardrails, business outcomes, cohort composition, and telemetry delay. I use explicit proceed, hold, or rollback criteria instead of promoting only because a timer expired.
What makes a percentage rollout rollback test complete?
A complete rollback proves the authoritative decision changes within the required propagation window and that clients, servers, workers, and caches return to stable behavior. It also verifies the old path can read or safely ignore data produced by treatment. The provider dashboard change is supporting evidence, not proof of application recovery.
Frequently Asked Questions
How do you test a feature flag percentage rollout?
Test deterministic assignment first, then verify behavior for named control and treatment identities, and finally compare observed evaluation events with the configured share. Include 0 and 100 percent boundaries, repeated evaluations, targeting precedence, distribution tolerance, staged expansion, and rollback. Use unique eligible identities as the allocation denominator.
Should a 10 percent rollout select exactly 10 out of every 100 users?
No. Hash-based rollout is not a quota for each small block of users. A large sample should be reasonably close to 10 percent, while any particular group of 100 can differ. Define a statistical tolerance and inspect the eligible population before declaring skew.
Why must percentage rollout assignment be sticky?
Stickiness keeps one identity on the same product path across requests and sessions, which prevents inconsistent UI, mixed data writes, and contaminated outcome comparisons. It requires a stable identity plus unchanged flag key, salt, and targeting configuration. Anonymous-to-authenticated transitions need an explicit identity policy.
What identity should a feature flag rollout use?
Prefer an immutable, non-sensitive account or tenant identifier that every evaluating service can resolve consistently. Avoid email, display name, and random request IDs. If device-level exposure is intentional, document how reinstalls, cookie deletion, and login affect assignment.
Can you test a managed feature flag provider with a custom hash?
A custom hash can test your own reference implementation, but it should not predict a vendor's undocumented buckets. For a managed provider, test public SDK behavior through an adapter, known targeting fixtures, decision reasons, stickiness, and observed exposure. Assert only contracts the provider documents.
How large should a feature flag distribution test sample be?
Choose enough deterministic identities for the smallest percentage to produce a useful expected count. This tutorial uses 100,000 fixtures, which gives about 1,000 expected members at 1 percent. The production decision still depends on real eligible volume, event quality, risk, and outcome latency.
How do you verify a percentage rollout rollback?
Reduce the percentage or disable the flag through the approved control plane, then measure when affected services return the stable decision. Verify stable UI and API behavior, background jobs, caches, and backward compatibility with data created by treatment. Save the audit event and observed recovery time.