adamcin opened a new pull request, #31:
URL: https://github.com/apache/sling-org-apache-sling-testing-osgi-mock/pull/31

   This PR adds a JUnit 5 extension for unit testing OSGi components which 
leverage R7 spec configuration annotations and [Component Property 
Types](https://docs.osgi.org/specification/osgi.cmpn/7.0.0/service.component.html#service.component-component.property.types).
   
   ## Usage
   
   Given an OSGi component that looks like this:
   
   ```java
   import org.osgi.service.component.annotations.Activate;
   import org.osgi.service.component.annotations.Component;
   import org.osgi.service.component.annotations.ConfigurationPolicy;
   import org.osgi.service.metatype.annotations.AttributeDefinition;
   import org.osgi.service.metatype.annotations.Designate;
   import org.osgi.service.metatype.annotations.ObjectClassDefinition;
   
   @Component(
           service = MyService.class,
           configurationPolicy = ConfigurationPolicy.REQUIRE
   )
   @Designate(MyService.Config.class)
   public class MyService {
   
       @ObjectClassDefinition
       public @interface Config {
   
           @AttributeDefinition(name = "My Config Value")
           String myConfigValue() default "default value";
       }
   
       private final String myConfigValue;
   
       @Activate
       public MyService(Config config) {
           this.myConfigValue = config.myConfigValue();
       }
   
       public String getMyConfigValue() {
           return myConfigValue;
       }
   }
   ```
   
   You can write a unit test for it like this:
   
   ```java
   import org.apache.sling.testing.mock.osgi.config.annotations.DynamicConfig;
   import 
org.apache.sling.testing.mock.osgi.junit5.OsgiConfigParametersExtension;
   import org.junit.jupiter.api.Assertions;
   import org.junit.jupiter.api.Test;
   import org.junit.jupiter.api.extension.ExtendWith;
   
   @ExtendWith(OsgiConfigParametersExtension.class)
   class MyServiceTest {
   
       @DynamicConfig(value = MyService.Config.class,
               property = "myConfigValue=my value")
       @Test
       void myServiceMethod(MyService.Config config) {
           MyService myService = new MyService(config);
           Assertions.assertEquals("my value", myService.getMyConfigValue());
       }
   }
   ```
   
   ## Using `@Retention(RetentionPolicy.RUNTIME)`
   
   Config annotations with `@Retention(RetentionPolicy.RUNTIME)` are supported. 
By modifying the `MyService.Config` type
   from the previous example:
   
   ```java
   @ObjectClassDefinition
   @Retention(RetentionPolicy.RUNTIME)
   public @interface Config {
   
       @AttributeDefinition(name = "My Config Value")
       String myConfigValue() default "default value";
   }
   ```
   
   You can rewrite the unit test to use the annotation directly, which skips 
the OSGi ConfigurationAdmin machinery:
   
   ```java
   import 
org.apache.sling.testing.mock.osgi.junit5.OsgiConfigParametersExtension;
   import org.junit.jupiter.api.Assertions;
   import org.junit.jupiter.api.Test;
   import org.junit.jupiter.api.extension.ExtendWith;
   
   @ExtendWith(OsgiConfigParametersExtension.class)
   class MyServiceTest {
   
       @MyService.Config(myConfigValue = "my value")
       @Test
       void myServiceMethod(MyService.Config config) {
           MyService myService = new MyService(config);
           Assertions.assertEquals("my value", myService.getMyConfigValue());
       }
   }
   ```
   
   ## Parameter Resolution
   
   ### Supported Parameter Types
   
   The extension searches the JUnit `ExtensionContext` for annotations that 
match a parameter type according to the following rules:
   
   | Parameter Type                                | Annotation Matching Rules  
                                                                                
                                                                                
                               |
   
|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
   | annotation                                    | annotation type is the 
same type as the parameter (exact match), or annotation is a `@DynamicConfig` 
with matching `value`                                                           
                                     |
   | interface                                     | annotation is a 
`@DynamicConfig` with matching `value`                                          
                                                                                
                                          |
   | array                                         | annotation type is the 
same type as the array component type (exact match), or annotation is a 
`@DynamicConfig` and its `value` specifies an annotation or interface type that 
matches the parameter array component type |
   | `@CollectConfigTypes({...}) ConfigCollection` | annotation type is 
specified in the `@CollectConfigTypes` value (exact match), or annotation is 
`@DynamicConfig` and its `value` is specified in the `@CollectConfigTypes` 
value                                          |
   
   ### Resolution Order
   
   For a given test method parameter, the extension will attempt to resolve the 
parameter value in the following order:
   
   1. Annotations are searched from top to bottom on the nearest executable 
context, which is the `@Test` method. Then the parent context is searched in 
the same way, then its parent context, and so on.
   2. If the parameter type is an annotation type or an interface, then the 
first exact match annotation or matching `@DynamicConfig` value is injected.
   3. If the parameter type is an annotation type or an interface, and is the 
same type as a previously resolved parameter in the same signature, then the 
*next* exact match annotation or matching `@DynamicConfig` value is injected.
   4. If the parameter type is an array of a matching parameter type, then all 
matching annotations are collected and their config values returned as an 
array, in the order of resolution as described in step #1.
   
   #### Example: `annotation` Parameter Type
   
   ```java
   import org.apache.sling.testing.mock.osgi.config.annotations.DynamicConfig;
   import 
org.apache.sling.testing.mock.osgi.junit5.OsgiConfigParametersExtension;
   import org.junit.jupiter.api.Assertions;
   import org.junit.jupiter.api.extension.ExtendWith;
   
   @DynamicConfig(value = MyService.Config.class, property = 
"myConfigValue=first class value")
   @MyService.Config(myConfigValue = "second class value")
   @ExtendWith(OsgiConfigParametersExtension.class)
   class MyServiceTest {
   
       @MyService.Config(myConfigValue = "first method value")
       @DynamicConfig(value = MyService.Config.class, property = 
"myConfigValue=second method value")
       @Test
       void myServiceMethod(MyService.Config config1,
                            MyService.Config config2,
                            MyService.Config config3,
                            MyService.Config config4) {
           MyService myService1 = new MyService(config1);
           Assertions.assertEquals("first method value", 
myService1.getMyConfigValue());
   
           MyService myService2 = new MyService(config2);
           Assertions.assertEquals("second method value", 
myService2.getMyConfigValue());
   
           MyService myService3 = new MyService(config3);
           Assertions.assertEquals("first class value", 
myService3.getMyConfigValue());
   
           MyService myService4 = new MyService(config4);
           Assertions.assertEquals("second class value", 
myService4.getMyConfigValue());
       }
   }
   ```
   
   #### Example: `interface` Parameter Type
   
   A less common example in practice, but one that is nonetheless supported by 
the extension, is to use an `interface` (not `@interface`) type as the target 
of `@Designate`.
   
   To inject an `interface` configuration to the test you must use a 
`@DynamicConfig` annotation, for obvious reasons.
   
   ```java
   import org.apache.sling.testing.mock.osgi.config.annotations.DynamicConfig;
   import 
org.apache.sling.testing.mock.osgi.junit5.OsgiConfigParametersExtension;
   import org.junit.jupiter.api.Assertions;
   import org.junit.jupiter.api.extension.ExtendWith;
   
   @DynamicConfig(value = MyConfigTest.MyConfig.class, property = 
"myConfigValue=class value")
   @ExtendWith(OsgiConfigParametersExtension.class)
   class MyConfigTest {
   
       interface MyConfig {
           String myConfigValue();
       }
   
       @DynamicConfig(value = MyConfigTest.MyConfig.class, property = 
"myConfigValue=method value")
       @Test
       void myConfigMethod(MyConfig config1, MyConfig config2) {
           Assertions.assertEquals("method value", config1.myConfigValue());
           Assertions.assertEquals("class value", config2.myConfigValue());
       }
   }
   ```
   
   #### Example: `array` Parameter Type
   
   When resolving multiple instances of the same config type, as long as it is 
an annotation or an interface, you can also
   use an array parameter type to collect all matching configs.
   
   ```java
   import org.apache.sling.testing.mock.osgi.config.annotations.DynamicConfig;
   import 
org.apache.sling.testing.mock.osgi.junit5.OsgiConfigParametersExtension;
   import org.junit.jupiter.api.Assertions;
   import org.junit.jupiter.api.extension.ExtendWith;
   
   import java.util.Arrays;
   import java.util.List;
   import java.util.stream.Collectors;
   
   @DynamicConfig(value = MyService.Config.class, property = 
"myConfigValue=first class value")
   @MyService.Config(myConfigValue = "second class value")
   @ExtendWith(OsgiConfigParametersExtension.class)
   class MyServiceTest {
   
       @MyService.Config(myConfigValue = "first method value")
       @DynamicConfig(value = MyService.Config.class, property = 
"myConfigValue=second method value")
       @Test
       void myServiceMethod(MyService.Config[] configs) {
           List<MyService> myServices = Arrays.stream(configs)
                   .map(MyService::new)
                   .collect(Collectors.toList());
           Assertions.assertEquals("first method value", 
myServices.get(0).getMyConfigValue());
           Assertions.assertEquals("second method value", 
myServices.get(1).getMyConfigValue());
           Assertions.assertEquals("first class value", 
myServices.get(2).getMyConfigValue());
           Assertions.assertEquals("second class value", 
myServices.get(3).getMyConfigValue());
       }
   }
   ```
   
   ### Repeated `@DynamicConfig` Annotations
   
   The extension allows a given `@Test` method to be annotated with 
`@DynamicConfig` multiple times, even with distinct `value` types. This feature 
is intended to support testing of OSGi Configuration Factory components.
   
   In this situation, declare a `@CollectConfigTypes({...}) ConfigCollection` 
test parameter to inject a `ConfigCollection` providing access to
   all configurations that match the values in the `@CollectConfigTypes` 
annotation. The test can then iterate over the configs in declaration order 
using `ConfigCollection#stream()`.
   
   ```java
   import 
org.apache.sling.testing.mock.osgi.config.annotations.CollectConfigTypes;
   import 
org.apache.sling.testing.mock.osgi.config.annotations.ConfigCollection;
   import org.apache.sling.testing.mock.osgi.config.annotations.DynamicConfig;
   import 
org.apache.sling.testing.mock.osgi.junit5.OsgiConfigParametersExtension;
   import org.junit.jupiter.api.Assertions;
   import org.junit.jupiter.api.extension.ExtendWith;
   
   import java.util.List;
   import java.util.stream.Collectors;
   
   @ExtendWith(OsgiConfigParametersExtension.class)
   class MyServiceTest {
   
       @DynamicConfig(value = MyService.Config.class, property = 
"myConfigValue=first method value")
       @DynamicConfig(value = MyService.Config.class, property = 
"myConfigValue=second method value")
       @DynamicConfig(value = MyService.Config.class, property = 
"myConfigValue=third method value")
       @DynamicConfig(value = MyService.Config.class, property = 
"myConfigValue=fourth method value")
       @Test
       void myServiceMethod(@CollectConfigTypes(MyService.Config.class)
                            ConfigCollection configs) {
           List<MyService> myServices = configs.stream()
                   .flatMap(entry -> entry.as(MyService.Config.class))
                   .map(ConfigCollection.Entry::getConfig)
                   .map(MyService::new)
                   .collect(Collectors.toList());
           Assertions.assertEquals("first method value", 
myServices.get(0).getMyConfigValue());
           Assertions.assertEquals("second method value", 
myServices.get(1).getMyConfigValue());
           Assertions.assertEquals("third method value", 
myServices.get(2).getMyConfigValue());
           Assertions.assertEquals("fourth method value", 
myServices.get(3).getMyConfigValue());
       }
   }
   ```
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: dev-unsubscr...@sling.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to