Testing API Reference¶
JUnit 5 extension and annotations for testing FlagZen features with pinned flag values.
Module: flagzen-test
Overview¶
The testing API provides:
FlagZenExtension— JUnit 5 extension for setting up test-scoped flag context@PinFlag— declarative annotation for pinning flag values in tests@FlagSource— load flag values from classpath properties filesTestFlagContext— programmatic API for setting and resolving flags
FlagZenExtension¶
JUnit 5 extension that manages test-scoped flag contexts and applies @PinFlag annotations.
Setup¶
Usage¶
Lifecycle¶
- Before each test method (
BeforeEachCallback): - Create a new
TestFlagContextwith an empty in-memory provider - Load flags from
@FlagSourceif present on the test class - Apply
@PinFlagannotations from the test method -
Store context in extension namespace
-
During test:
- Test can inject
TestFlagContextor feature types as method parameters -
Extension resolves them via
ParameterResolver -
After each test method (
AfterEachCallback): - Clean up the test context (remove from namespace)
Feature Type Discovery¶
The extension discovers all feature types via ServiceLoader<FeatureMetadata>. Feature types are those generated by the annotation processor for each @Feature interface.
@ExtendWith(FlagZenExtension.class)
class CheckoutTest {
@Test
void testCheckoutFlow(CheckoutFlow flow) {
// CheckoutFlow is automatically discovered and resolved
assertNotNull(flow);
}
}
@PinFlag¶
Pins a feature flag to a specific variant value for the duration of a test method.
Annotation¶
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(PinFlags.class)
public @interface PinFlag {
String feature();
String variant();
}
Attributes¶
| Attribute | Type | Description |
|---|---|---|
feature |
String | The feature flag key to pin (e.g., "checkout-flow") |
variant |
String | The variant value to pin (e.g., "CLASSIC") |
Usage¶
@ExtendWith(FlagZenExtension.class)
class CheckoutTest {
@Test
@PinFlag(feature = "checkout-flow", variant = "CLASSIC")
void testClassicFlow(CheckoutFlow flow) {
assertEquals("classic", flow.execute());
}
@Test
@PinFlag(feature = "checkout-flow", variant = "PREMIUM")
void testPremiumFlow(CheckoutFlow flow) {
assertEquals("premium", flow.execute());
}
}
Multiple Pins¶
Use @Repeatable to pin multiple flags in one test:
@Test
@PinFlag(feature = "checkout-flow", variant = "PREMIUM")
@PinFlag(feature = "dark-mode", variant = "true")
void testPremiumDarkCheckout(CheckoutFlow flow, DarkMode theme) {
assertEquals("premium", flow.execute());
assertEquals("dark", theme.theme());
}
Priority¶
The priority order for flag values is:
@PinFlagon the test method (highest priority)@FlagSourceon the test class- Empty provider (all flags absent)
If a flag is pinned by both @PinFlag and @FlagSource, the @PinFlag value wins.
@FlagSource¶
Loads flag values from a classpath properties file for all tests in the annotated class or method.
Annotation¶
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface FlagSource {
String value();
}
Attributes¶
| Attribute | Type | Description |
|---|---|---|
value |
String | Classpath resource path to the properties file (e.g., "flags.properties") |
File Format¶
Plain Java properties file format. Each line is flag.key=variant.value:
Usage¶
Class-level loading (applies to all test methods):
@ExtendWith(FlagZenExtension.class)
@FlagSource("test-flags.properties")
class AllCheckoutTests {
@Test
void testCheckoutFlow(CheckoutFlow flow) {
assertEquals("CLASSIC", flow.execute());
}
// All tests use flags from test-flags.properties
}
Method-level override:
@ExtendWith(FlagZenExtension.class)
@FlagSource("default-flags.properties")
class AllCheckoutTests {
@Test
void testDefault(CheckoutFlow flow) {
// Uses flags from default-flags.properties
}
@Test
@FlagSource("premium-flags.properties")
void testPremium(CheckoutFlow flow) {
// Uses flags from premium-flags.properties (overrides class-level)
}
}
Error Handling¶
- Throws
FlagZenExceptionif the resource file is not found - Throws
IllegalStateExceptionif the file cannot be read - Logs the resource path in the error message for debugging
TestFlagContext¶
Programmatic API for pinning flag values and resolving features in tests.
Creating a Context¶
Empty context (for programmatic pin-then-test patterns):
TestFlagContext context = TestFlagContext.create();
context.pin("checkout-flow", "CLASSIC");
CheckoutFlow flow = context.resolve(CheckoutFlow.class);
assertEquals("classic", flow.execute());
From properties file:
TestFlagContext context = TestFlagContext.createFromProperties("test-flags.properties");
CheckoutFlow flow = context.resolve(CheckoutFlow.class);
Methods¶
pin(String key, String value)¶
Pins a flag to a value.
Signature
Parameters
key(String): The flag keyvalue(String): The variant value
Example
TestFlagContext context = TestFlagContext.create();
context.pin("checkout-flow", "CLASSIC");
context.pin("dark-mode", "true");
context.pin("max-retries", "5");
resolve(Class<T> featureType)¶
Resolves a feature interface to its proxy using the pinned flag values.
Signature
Type Parameter
T: The feature interface type
Parameters
featureType(Class<T>): The feature interface class
Returns
- A proxy delegating to the active variant based on pinned flag values
Example
TestFlagContext context = TestFlagContext.create();
context.pin("checkout-flow", "PREMIUM");
CheckoutFlow flow = context.resolve(CheckoutFlow.class);
assertEquals("premium", flow.execute());
Injection via Extension¶
Do not create TestFlagContext manually when using FlagZenExtension. Instead, inject it as a parameter:
@ExtendWith(FlagZenExtension.class)
class CheckoutTest {
@Test
void testWithContext(TestFlagContext context) {
context.pin("checkout-flow", "CLASSIC");
CheckoutFlow flow = context.resolve(CheckoutFlow.class);
assertEquals("classic", flow.execute());
}
}
Complete Example¶
@ExtendWith(FlagZenExtension.class)
@FlagSource("integration-test-flags.properties")
class CheckoutIntegrationTest {
// Uses flags from integration-test-flags.properties
@Test
void testDefaultCheckout(CheckoutFlow flow) {
String result = flow.execute();
assertNotNull(result);
}
// Pins checkout-flow to CLASSIC, overrides @FlagSource
@Test
@PinFlag(feature = "checkout-flow", variant = "CLASSIC")
void testClassicCheckout(CheckoutFlow flow) {
assertEquals("classic", flow.execute());
}
// Pins multiple flags
@Test
@PinFlag(feature = "checkout-flow", variant = "PREMIUM")
@PinFlag(feature = "dark-mode", variant = "true")
void testPremiumDarkCheckout(
CheckoutFlow flow,
DarkMode theme,
TestFlagContext context) {
// Programmatically adjust if needed
context.pin("max-retries", "10");
assertEquals("premium", flow.execute());
assertEquals("dark", theme.theme());
}
// Use TestFlagContext directly
@Test
void testProgrammaticFlags(TestFlagContext context) {
context.pin("checkout-flow", "STREAMLINED");
CheckoutFlow flow = context.resolve(CheckoutFlow.class);
assertEquals("streamlined", flow.execute());
}
}
Best Practices¶
- Use
@PinFlagfor simple cases: When you just need to set one or two flags for a test. - Use
@FlagSourcefor complex scenarios: When multiple tests share a large set of flag values, load from a properties file instead of repeating@PinFlagannotations. - Use
TestFlagContextfor dynamic scenarios: When you need to change flag values during a test or set them conditionally. - Name properties files clearly: Use names like
test-flags.properties,smoke-test-flags.propertiesto indicate their purpose. - Keep flags organized: Group related flags in the same properties file or in the same test class.
- Test both true and false for booleans: Create separate test methods pinning boolean features to both
"true"and"false". - Test variants independently: Create one test per variant to ensure each variant implementation works correctly.
Thread Safety¶
TestFlagContextis thread-scoped via theFlagZenExtension- Each test method gets its own context
- Safe for parallel test execution (each test is isolated)
Related¶
FlagZenExtension— JUnit 5 integration@PinFlag— declarative flag pinning@FlagSource— properties file loadingTestFlagContext— programmatic APIFeatureDispatcher— runtime dispatch