-   Notifications  
You must be signed in to change notification settings  - Fork 58
 
feat: Add JUnit5 extension for OpenFeature #888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
   Merged  
  beeme1mr merged 10 commits into open-feature:main from open-feature-forking:feat/junit-extension        Jul 26, 2024  
    Merged  
 Changes from 4 commits
 Commits 
  Show all changes 
  10 commits   Select commit Hold shift + click to select a range 
 b177459  feat: Add JUnit5 extension for OpenFeature 
  aepfli 61a7bc0  chore: checkstyle issues 
  aepfli 5787a4a  chore: more types and tests 
  aepfli b9dbf75  chore: adding own testProvider with some Thread magic 
  aepfli 01b9e6f  chore: changing to inmemory provider wrapper for testprovider and pr … 
  aepfli 49b9db0  Merge branch 'main' into feat/junit-extension 
  aepfli 74a9ff8  chore: changing to inmemory provider wrapper for testprovider and pr … 
  aepfli 93f10b9  chore: fix release-please files etc 
  aepfli 27dcd9a  Apply suggestions from code review 
  aepfli 05f94cc  Merge branch 'main' into feat/junit-extension 
  aepfli File filter
Filter by extension
Conversations
 Failed to load comments.  
    Loading  
 Jump to
  Jump to file  
  Failed to load files.  
    Loading  
 Diff view
Diff view
There are no files selected for viewing
   This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters   
       Submodule test-harness updated  from ed7e0b to 25544e  
    This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters   
     | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| # JUnit Open Feature extension | ||
|   |  ||
| A JUnit5 extension to reduce boilerplate code for testing code which utilizes OpenFeature. | ||
|   |  ||
| ## Getting Started | ||
|   |  ||
| We are supporting two different flavors for testing, a [simple](#simple-configuration) and an [extended](#extended-configuration) configuration. | ||
|   |  ||
| Notice: We are most likely not multithread compatible! | ||
| ### Simple Configuration | ||
|   |  ||
| Choose the simple configuration if you are only testing in one domain. | ||
| Per default, it will be used in the global domain. | ||
|   |  ||
| ```java | ||
| @Test | ||
| @Flag(name = "BOOLEAN_FLAG", value = "true") | ||
| void test() { | ||
| // your test code | ||
| } | ||
| ``` | ||
|   |  ||
| #### Multiple flags | ||
|   |  ||
| The `@Flag` annotation can be also repeated multiple times. | ||
|   |  ||
| ```java | ||
| @Test | ||
| @Flag(name = "BOOLEAN_FLAG", value = "true") | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "true") | ||
| void test() { | ||
| // your test code | ||
| } | ||
| ``` | ||
|   |  ||
| #### Defining Flags for a whole test-class | ||
|   |  ||
| `@Flags` can be defined on the class-level too, but method-level | ||
| annotations will superseded class-level annotations. | ||
    aepfli marked this conversation as resolved.    Outdated   Show resolved Hide resolved  |  ||
|   |  ||
| ```java | ||
| @Flag(name = "BOOLEAN_FLAG", value = "true") | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "false") | ||
| class Test { | ||
| @Test | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "true") // will be used | ||
| void test() { | ||
| // your test code | ||
| } | ||
| } | ||
| ``` | ||
|   |  ||
| #### Setting a different domain | ||
|   |  ||
| You can define an own domain on the test-class-level with `@OpenFeatureDefaultDomain` like: | ||
    aepfli marked this conversation as resolved.    Outdated   Show resolved Hide resolved  |  ||
|   |  ||
| ```java | ||
| @OpenFeatureDefaultDomain("domain") | ||
| class Test { | ||
| @Test | ||
| @Flag(name = "BOOLEAN_FLAG", value = "true") | ||
| // this flag will be available in the `domain` domain | ||
| void test() { | ||
| // your test code | ||
| } | ||
| } | ||
| ``` | ||
|   |  ||
| ### Extended Configuration | ||
|   |  ||
| Use the extended configuration when your code needs to use multiple domains. | ||
|   |  ||
| ```java | ||
| @Test | ||
| @OpenFeature({ | ||
| @Flag(name = "BOOLEAN_FLAG", value = "true") | ||
| }) | ||
| void test() { | ||
| // your test code | ||
| } | ||
| ``` | ||
    aepfli marked this conversation as resolved.    Outdated   Show resolved Hide resolved  |  ||
|   |  ||
|   |  ||
| #### Multiple flags | ||
|   |  ||
| The `@Flag` annotation can be also repeated multiple times. | ||
|   |  ||
| ```java | ||
| @Test | ||
| @OpenFeature({ | ||
| @Flag(name = "BOOLEAN_FLAG", value = "true"), | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "true") | ||
| }) | ||
| void test() { | ||
| // your test code | ||
| } | ||
| ``` | ||
|   |  ||
| #### Defining Flags for a whole test-class | ||
|   |  ||
| `@Flags` can be defined on the class-level too, but method-level | ||
    aepfli marked this conversation as resolved.    Outdated   Show resolved Hide resolved  |  ||
| annotations will superseded class-level annotations. | ||
|   |  ||
| ```java | ||
| @OpenFeature({ | ||
| @Flag(name = "BOOLEAN_FLAG", value = "true"), | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "false") | ||
| }) | ||
| class Test { | ||
| @Test | ||
| @OpenFeature({ | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "true") // will be used | ||
| }) | ||
| void test() { | ||
| // your test code | ||
| } | ||
| } | ||
| ``` | ||
|   |  ||
| #### Setting a different domain | ||
|   |  ||
| You can define an own domain for each usage of the `@OpenFeature` annotation with the `domain` property: | ||
|   |  ||
| ```java | ||
| @Test | ||
| @OpenFeature( | ||
| domain = "domain", | ||
| value = { | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "true") // will be used | ||
| }) | ||
| // this flag will be available in the `domain` domain | ||
| void test() { | ||
| // your test code | ||
| } | ||
| ``` | ||
|   |  ||
| #### Multiple Configurations for multiple domains | ||
|   |  ||
| Following testcode will generate two providers, with different flag configurations for a test. | ||
|   |  ||
| ```java | ||
| @Test | ||
| @OpenFeature({ | ||
| @Flag(name = "BOOLEAN_FLAG", value = "true"), | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "true") | ||
| }) | ||
| @OpenFeature( | ||
| domain = "domain", | ||
| value = { | ||
| @Flag(name = "BOOLEAN_FLAG2", value = "true") // will be used | ||
| }) | ||
| void test() { | ||
| // your test code | ||
| } | ||
| ``` | ||
|   |  ||
   This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters   
     | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
| <modelVersion>4.0.0</modelVersion> | ||
| <parent> | ||
| <groupId>dev.openfeature.contrib</groupId> | ||
| <artifactId>parent</artifactId> | ||
| <version>0.1.0</version> | ||
| <relativePath>../../pom.xml</relativePath> | ||
| </parent> | ||
| <groupId>dev.openfeature.contrib.tools</groupId> | ||
| <artifactId>junitopenfeature</artifactId> | ||
| <version>0.0.1</version> <!--x-release-please-version --> | ||
|   |  ||
| <name>junit-openfeature-extension</name> | ||
| <description>JUnit5 Extension for OpenFeature</description> | ||
| <url>https://openfeature.dev</url> | ||
|   |  ||
| <developers> | ||
| <developer> | ||
| <id>aepfli</id> | ||
| <name>Simon Schrottner</name> | ||
| <organization>OpenFeature</organization> | ||
| <url>https://openfeature.dev/</url> | ||
| </developer> | ||
| </developers> | ||
|   |  ||
| <dependencies> | ||
| <dependency> | ||
| <groupId>dev.openfeature</groupId> | ||
| <artifactId>sdk</artifactId> | ||
| <version>[1.4,2.0)</version> | ||
| <scope>provided</scope> | ||
| </dependency> | ||
|   |  ||
| <dependency> | ||
| <groupId>org.junit.jupiter</groupId> | ||
| <artifactId>junit-jupiter-api</artifactId> | ||
| <version>${junit.jupiter.version}</version> | ||
| </dependency> | ||
|   |  ||
| <dependency> | ||
| <groupId>org.junit-pioneer</groupId> | ||
| <artifactId>junit-pioneer</artifactId> | ||
| <version>1.9.1</version> | ||
| </dependency> | ||
| </dependencies> | ||
|   |  ||
| </project> | 
       155 changes: 155 additions & 0 deletions  155   ...re/src/main/java/dev/openfeature/contrib/tools/junitopenfeature/OpenFeatureExtension.java          
     This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters   
     | Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| package dev.openfeature.contrib.tools.junitopenfeature; | ||
|   |  ||
| import dev.openfeature.contrib.tools.junitopenfeature.annotations.OpenFeature; | ||
| import dev.openfeature.contrib.tools.junitopenfeature.annotations.OpenFeatureDefaultDomain; | ||
| import dev.openfeature.sdk.OpenFeatureAPI; | ||
| import dev.openfeature.sdk.providers.memory.Flag; | ||
| import org.apache.commons.lang3.BooleanUtils; | ||
| import org.junit.jupiter.api.extension.AfterEachCallback; | ||
| import org.junit.jupiter.api.extension.BeforeEachCallback; | ||
| import org.junit.jupiter.api.extension.ExtensionContext; | ||
| import org.junit.jupiter.api.extension.InvocationInterceptor; | ||
| import org.junit.jupiter.api.extension.ReflectiveInvocationContext; | ||
| import org.junitpioneer.internal.PioneerAnnotationUtils; | ||
|   |  ||
| import java.lang.reflect.Method; | ||
| import java.util.Arrays; | ||
| import java.util.HashMap; | ||
| import java.util.Map; | ||
|   |  ||
| /** | ||
| * JUnit5 Extension for OpenFeature. | ||
| */ | ||
| public class OpenFeatureExtension implements BeforeEachCallback, AfterEachCallback, InvocationInterceptor { | ||
|   |  ||
| OpenFeatureAPI api = OpenFeatureAPI.getInstance(); | ||
|   |  ||
| private static Map<String, Map<String, Flag<?>>> handleExtendedConfiguration( | ||
| ExtensionContext extensionContext, | ||
| Map<String, Map<String, Flag<?>>> configuration | ||
| ) { | ||
| PioneerAnnotationUtils | ||
| .findAllEnclosingRepeatableAnnotations(extensionContext, OpenFeature.class) | ||
| .forEachOrdered(annotation -> { | ||
| Map<String, Flag<?>> domainFlags = configuration.getOrDefault(annotation.domain(), new HashMap<>()); | ||
|   |  ||
| Arrays.stream(annotation.value()) | ||
| .filter(flag -> !domainFlags.containsKey(flag.name())) | ||
| .forEach(flag -> { | ||
| Flag.FlagBuilder<?> builder = generateFlagBuilder(flag); | ||
| domainFlags.put(flag.name(), builder.build()); | ||
| }); | ||
| configuration.put(annotation.domain(), domainFlags); | ||
| }); | ||
| return configuration; | ||
| } | ||
|   |  ||
| private static Map<String, Map<String, Flag<?>>> handleSimpleConfiguration(ExtensionContext extensionContext) { | ||
| Map<String, Map<String, Flag<?>>> configuration = new HashMap<>(); | ||
| String defaultDomain = PioneerAnnotationUtils | ||
| .findClosestEnclosingAnnotation(extensionContext, OpenFeatureDefaultDomain.class) | ||
| .map(OpenFeatureDefaultDomain::value).orElse(""); | ||
| PioneerAnnotationUtils | ||
| .findAllEnclosingRepeatableAnnotations( | ||
| extensionContext, | ||
| dev.openfeature.contrib.tools.junitopenfeature.annotations.Flag.class) | ||
| .forEachOrdered(flag -> { | ||
| Map<String, Flag<?>> domainFlags = configuration.getOrDefault(defaultDomain, new HashMap<>()); | ||
| if (!domainFlags.containsKey(flag.name())) { | ||
| Flag.FlagBuilder<?> builder = generateFlagBuilder(flag); | ||
| domainFlags.put(flag.name(), builder.build()); | ||
| configuration.put(defaultDomain, domainFlags); | ||
| } | ||
| }); | ||
|   |  ||
| return configuration; | ||
| } | ||
|   |  ||
| private static Flag.FlagBuilder<?> generateFlagBuilder( | ||
| dev.openfeature.contrib.tools.junitopenfeature.annotations.Flag flag | ||
| ) { | ||
| Flag.FlagBuilder<?> builder; | ||
| switch (flag.valueType().getSimpleName()) { | ||
| case "Boolean": | ||
| builder = Flag.<Boolean>builder(); | ||
| builder.variant(flag.value(), BooleanUtils.toBoolean(flag.value())); | ||
| break; | ||
| case "String": | ||
| builder = Flag.<String>builder(); | ||
| builder.variant(flag.value(), flag.value()); | ||
| break; | ||
| case "Integer": | ||
| builder = Flag.<Integer>builder(); | ||
| builder.variant(flag.value(), Integer.parseInt(flag.value())); | ||
| break; | ||
| case "Double": | ||
| builder = Flag.<Double>builder(); | ||
| builder.variant(flag.value(), Double.parseDouble(flag.value())); | ||
| break; | ||
| default: | ||
| throw new IllegalArgumentException("Unsupported flag type: " + flag.value()); | ||
| } | ||
| builder.defaultVariant(flag.value()); | ||
| return builder; | ||
| } | ||
|   |  ||
| @Override | ||
| public void interceptTestMethod( | ||
| Invocation<Void> invocation, | ||
| ReflectiveInvocationContext<Method> invocationContext, | ||
| ExtensionContext extensionContext | ||
| ) throws Throwable { | ||
| TestProvider.CURRENT_NAMESPACE.set(getNamespace(extensionContext)); | ||
| invocation.proceed(); | ||
| TestProvider.CURRENT_NAMESPACE.remove(); | ||
| } | ||
|   |  ||
| @Override | ||
| public void afterEach(ExtensionContext extensionContext) throws Exception { | ||
| } | ||
|   |  ||
| @Override | ||
| public void beforeEach(ExtensionContext extensionContext) throws Exception { | ||
| Map<String, Map<String, Flag<?>>> configuration = handleSimpleConfiguration(extensionContext); | ||
| configuration.putAll(handleExtendedConfiguration(extensionContext, configuration)); | ||
|   |  ||
| for (Map.Entry<String, Map<String, Flag<?>>> stringMapEntry : configuration.entrySet()) { | ||
|   |  ||
| if (!stringMapEntry.getKey().isEmpty()) { | ||
| String domain = stringMapEntry.getKey(); | ||
| if (api.getProvider(domain) instanceof TestProvider && api.getProvider(domain) != api.getProvider()) { | ||
| ((TestProvider) api.getProvider(domain)) | ||
| .addFlags(getNamespace(extensionContext), stringMapEntry.getValue()); | ||
| } else { | ||
| api.setProvider(domain, new TestProvider( | ||
| getNamespace(extensionContext), | ||
| stringMapEntry.getValue())); | ||
| } | ||
| } else { | ||
| if (api.getProvider() instanceof TestProvider) { | ||
| ((TestProvider) api.getProvider()) | ||
| .addFlags(getNamespace(extensionContext), stringMapEntry.getValue()); | ||
| } else { | ||
| api.setProvider(new TestProvider( | ||
| getNamespace(extensionContext), | ||
| stringMapEntry.getValue())); | ||
| } | ||
| } | ||
|   |  ||
| } | ||
|   |  ||
| getStore(extensionContext).put("config", configuration); | ||
|   |  ||
| } | ||
|   |  ||
| private ExtensionContext.Namespace getNamespace(ExtensionContext extensionContext) { | ||
| return ExtensionContext.Namespace.create( | ||
| getClass(), | ||
| extensionContext.getRequiredTestMethod() | ||
| ); | ||
| } | ||
|   |  ||
| private ExtensionContext.Store getStore(ExtensionContext context) { | ||
| return context.getStore(ExtensionContext.Namespace.create(getClass())); | ||
| } | ||
| } | 
  Oops, something went wrong.  
  Add this suggestion to a batch that can be applied as a single commit. This suggestion is invalid because no changes were made to the code. Suggestions cannot be applied while the pull request is closed. Suggestions cannot be applied while viewing a subset of changes. Only one suggestion per line can be applied in a batch. Add this suggestion to a batch that can be applied as a single commit. Applying suggestions on deleted lines is not supported. You must change the existing code in this line in order to create a valid suggestion. Outdated suggestions cannot be applied. This suggestion has been applied or marked resolved. Suggestions cannot be applied from pending reviews. Suggestions cannot be applied on multi-line comments. Suggestions cannot be applied while the pull request is queued to merge. Suggestion cannot be applied right now. Please check back later.    
 
Uh oh!
There was an error while loading. Please reload this page.