You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by js...@apache.org on 2021/06/16 14:00:09 UTC

[sling-org-apache-sling-junit-core] 01/01: SLING-10497 - JUnit Jupiter ParameterResolver for OSGi

This is an automated email from the ASF dual-hosted git repository.

jsedding pushed a commit to branch feature/SLING-10497-jupiter-parameter-resolver-for-osgi
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-junit-core.git

commit 3ed448a258a8503684893ffd6c41b46680fc9422
Author: Julian Sedding <js...@apache.org>
AuthorDate: Wed Jun 16 15:58:46 2021 +0200

    SLING-10497 - JUnit Jupiter ParameterResolver for OSGi
---
 bnd.bnd                                            |   2 +
 pom.xml                                            |  22 +-
 .../junit5/JUnit5TestExecutionStrategy.java        |  45 +--
 .../impl/servlet/junit5/JUnitPlatformHelper.java   | 117 ++++++
 .../org/apache/sling/junit/jupiter/osgi/OSGi.java  |  53 +++
 .../apache/sling/junit/jupiter/osgi/Service.java   |  72 ++++
 .../apache/sling/junit/jupiter/osgi/Services.java  |  36 ++
 .../impl/AbstractTypeBasedParameterResolver.java   |  66 ++++
 .../osgi/impl/BundleContextParameterResolver.java  |  33 ++
 .../jupiter/osgi/impl/BundleParameterResolver.java |  33 ++
 .../junit/jupiter/osgi/impl/ReflectionHelper.java  | 103 ++++++
 .../osgi/impl/ServiceParameterResolver.java        | 263 ++++++++++++++
 .../osgi/impl/TypeBasedParameterResolver.java      |  53 +++
 .../sling/junit/jupiter/osgi/package-info.java     |  22 ++
 .../sling/junit/impl/servlet/HtmlRendererTest.java |  16 +-
 .../junit/jupiter/osgi/OSGiAnnotationTest.java     | 392 +++++++++++++++++++++
 16 files changed, 1270 insertions(+), 58 deletions(-)

diff --git a/bnd.bnd b/bnd.bnd
index aea6f89..196c623 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -1,8 +1,10 @@
 Bundle-Activator: org.apache.sling.junit.Activator
 Export-Package: !org.junit.platform.*, \
+                !org.junit.jupiter.*, \
                 junit.*;version=${junit.version}, \
                 org.junit.*;version=${junit.version}, \
                 org.hamcrest.*;version=${hamcrest.version};-split-package:=merge-first
 Import-Package: org.junit.platform.*;resolution:=optional, \
+                org.junit.jupiter.*;resolution:=optional, \
                 *
 -includeresource: @org.jacoco.agent-*.jar!/org/jacoco/agent/rt/IAgent*
diff --git a/pom.xml b/pom.xml
index 9e68ab1..4428527 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,7 +37,7 @@
         <junit.version>4.13</junit.version>
         <hamcrest.version>1.3</hamcrest.version>
         <jacoco.version>0.6.2.201302030002</jacoco.version>
-        <junit-jupiter.version>5.7.1</junit-jupiter.version>
+        <junit-jupiter.version>5.5.0</junit-jupiter.version>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
     </properties>
@@ -285,6 +285,13 @@
             <version>2.2.2</version>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+            <version>3.0</version>
+            <scope>provided</scope>
+        </dependency>
+
         <!-- optional imports for JUnit 5 support -->
         <dependency>
             <groupId>org.junit.platform</groupId>
@@ -319,7 +326,7 @@
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter-api</artifactId>
             <version>${junit-jupiter.version}</version>
-            <scope>test</scope>
+            <scope>provided</scope>
         </dependency>
 
         <dependency>
@@ -345,8 +352,15 @@
 
         <dependency>
             <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <version>3.5.7</version>
+            <artifactId>mockito-inline</artifactId>
+            <version>3.10.0</version>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.osgi-mock.junit5</artifactId>
+            <version>3.1.2</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
index 7363374..e39ed73 100644
--- a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnit5TestExecutionStrategy.java
@@ -21,22 +21,14 @@ package org.apache.sling.junit.impl.servlet.junit5;
 import org.apache.sling.junit.TestSelector;
 import org.apache.sling.junit.impl.TestExecutionStrategy;
 import org.apache.sling.junit.impl.TestsManagerImpl;
-import org.jetbrains.annotations.NotNull;
-import org.junit.platform.engine.DiscoverySelector;
-import org.junit.platform.engine.TestEngine;
-import org.junit.platform.engine.discovery.DiscoverySelectors;
 import org.junit.platform.launcher.Launcher;
 import org.junit.platform.launcher.LauncherDiscoveryRequest;
-import org.junit.platform.launcher.core.LauncherConfig;
-import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
-import org.junit.platform.launcher.core.LauncherFactory;
 import org.junit.runner.notification.RunListener;
 import org.osgi.framework.BundleContext;
 
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
-import java.util.stream.Stream;
 
 import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
 
@@ -73,39 +65,10 @@ public class JUnit5TestExecutionStrategy implements TestExecutionStrategy {
 
     @Override
     public void execute(TestSelector selector, RunListener runListener) throws Exception {
-        Launcher launcher = createLauncher(runListener, testEngineTracker.getAvailableTestEngines());
+        Launcher launcher = JUnitPlatformHelper.createLauncher(testEngineTracker.getAvailableTestEngines());
         final LauncherDiscoveryRequest request = testsManager.createTestRequest(selector,
-                JUnit5TestExecutionStrategy::methodRequest,
-                JUnit5TestExecutionStrategy::classesRequest);
-        launcher.execute(request);
-    }
-
-    @NotNull
-    public static Launcher createLauncher(RunListener runListener, TestEngine... availableTestEngines) {
-        return LauncherFactory.create(
-                LauncherConfig.builder()
-                        .addTestEngines(availableTestEngines)
-                        .addTestExecutionListeners(new RunListenerAdapter(runListener))
-                        .enableTestEngineAutoRegistration(false)
-                        .enableTestExecutionListenerAutoRegistration(false)
-                        .build()
-        );
-    }
-
-    @NotNull
-    public static LauncherDiscoveryRequest methodRequest(Class<?> testClass, String testMethodName) {
-        return LauncherDiscoveryRequestBuilder.request()
-                .selectors(selectMethod(testClass, testMethodName))
-                .build();
-    }
-
-    @NotNull
-    public static LauncherDiscoveryRequest classesRequest(Class<?>... testClasses) {
-        final DiscoverySelector[] selectors = Stream.of(testClasses)
-                .map(DiscoverySelectors::selectClass)
-                .toArray(DiscoverySelector[]::new);
-        return LauncherDiscoveryRequestBuilder.request()
-                .selectors(selectors)
-                .build();
+                JUnitPlatformHelper::methodRequest,
+                JUnitPlatformHelper::classesRequest);
+        launcher.execute(request, new RunListenerAdapter(runListener));
     }
 }
diff --git a/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnitPlatformHelper.java b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnitPlatformHelper.java
new file mode 100644
index 0000000..f6c0312
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/impl/servlet/junit5/JUnitPlatformHelper.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.impl.servlet.junit5;
+
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.platform.commons.util.ReflectionUtils;
+import org.junit.platform.engine.DiscoverySelector;
+import org.junit.platform.engine.TestEngine;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.core.LauncherConfig;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+
+import java.util.Objects;
+import java.util.stream.Stream;
+
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod;
+
+/**
+ * Utilities for running tests via the JUnit Platform. I.e. depending on the supplied {@link TestEngine}(s),
+ * it is possible to run JUnit3, JUnit4 or Jupiter based tests using {@code TestEngines} supplied by
+ * the JUnit team. No issues are expected running bespoke {@code TestEngines}.
+ */
+public final class JUnitPlatformHelper {
+
+    /**
+     * Execute a test class (if {@code testMethodName == null}) or a single test method with a specified {@code TestEngine}.
+     * All provided {@code TestExecutionListener}s are registered to be notified of the test's execution.
+     *
+     * @param testEngine  a {@code TestEngine} instance
+     * @param testClass   a test class that can be executed by the given {@code TestEngine}
+     * @param testMethodName  the name of a test method in the given test class or null to run all test methods
+     * @param listeners   any number of {@code TestExecutionListener}s that should be notified
+     */
+    public static void executeTest(@NotNull TestEngine testEngine, @NotNull Class<?> testClass, @Nullable String testMethodName, @NotNull TestExecutionListener... listeners) {
+        final Launcher launcher = JUnitPlatformHelper.createLauncher(testEngine);
+        final LauncherDiscoveryRequest request = testMethodName != null
+                ? JUnitPlatformHelper.methodRequest(testClass, testMethodName)
+                : JUnitPlatformHelper.classesRequest(testClass);
+        launcher.execute(request, listeners);
+    }
+
+    /**
+     * Utility method to create a {@link Launcher} for the given {@code TestEngines} only, without
+     * any automatically registered {@code TestEngines} or {@code TestExecutionListeners}.
+     *
+     * @param testEngines The test engines available to the {@code Launcher} instance.
+     * @return A JUnit Platform {@code Launcher} instance.
+     */
+    @NotNull
+    public static Launcher createLauncher(TestEngine... testEngines) {
+        return LauncherFactory.create(LauncherConfig.builder()
+                .enableTestEngineAutoRegistration(false)
+                .enableTestExecutionListenerAutoRegistration(false)
+                .addTestEngines(testEngines)
+                .build());
+    }
+
+    /**
+     * Utility to create a {@link LauncherDiscoveryRequest} for a particular test method, specified by the
+     * test class and the test method's name. If multiple overloaded test methods with different parameters
+     * exist, they would all be executed.
+     *
+     * @param testClass   a test class
+     * @param testMethodName  the name of a test method in the given test class or null to run all test methods
+     * @return a {@code LauncherDiscoveryRequest} representing the specified test method.
+     */
+    @NotNull
+    public static LauncherDiscoveryRequest methodRequest(Class<?> testClass, String testMethodName) {
+        final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request();
+        ReflectionUtils.findMethods(testClass, method -> Objects.equals(method.getName(), testMethodName)).stream()
+                .map(method -> selectMethod(testClass, method))
+                .forEach(requestBuilder::selectors);
+        return requestBuilder.build();
+    }
+
+
+    /**
+     * Utility to create a {@link LauncherDiscoveryRequest} for all test methods of the specified test class(es).
+     *
+     * @param testClasses   a number of test classes
+     * @return a {@code LauncherDiscoveryRequest} representing the specified test classes.
+     */
+    @NotNull
+    public static LauncherDiscoveryRequest classesRequest(Class<?>... testClasses) {
+        final DiscoverySelector[] selectors = Stream.of(testClasses)
+                .map(DiscoverySelectors::selectClass)
+                .toArray(DiscoverySelector[]::new);
+        return LauncherDiscoveryRequestBuilder.request()
+                .selectors(selectors)
+                .build();
+    }
+
+    private JUnitPlatformHelper() {
+        // no instances
+    }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/OSGi.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/OSGi.java
new file mode 100644
index 0000000..f5a07ae
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/OSGi.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi;
+
+import org.apache.sling.junit.jupiter.osgi.impl.BundleContextParameterResolver;
+import org.apache.sling.junit.jupiter.osgi.impl.BundleParameterResolver;
+import org.apache.sling.junit.jupiter.osgi.impl.ServiceParameterResolver;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * OSGi test annotation, for running unit tests within OSGi frameworks. The annotation supports
+ * injecting {@link Bundle}, {@link BundleContext} and service instances in conjunction with the
+ * {@link Service @Service} annotation. The annotation can be used on test classes or on individual
+ * test methods. If used on test classes injection of constructor parameters is supported in addition
+ * to injection of method parameters.
+ * <br>
+ * Note: the implementation relies on calling {@link FrameworkUtil#getBundle(Class)} with the test class
+ * in order to gain access to the world of OSGi.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD})
+@ExtendWith({
+        BundleParameterResolver.class,
+        BundleContextParameterResolver.class,
+        ServiceParameterResolver.class
+})
+@Inherited
+public @interface OSGi {}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java
new file mode 100644
index 0000000..3e901c3
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi;
+
+import org.osgi.framework.Filter;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
+
+/**
+ * The {@code @Service} annotation is to be used for test classes or methods annotated
+ * with the {@link OSGi @OSGi} annotation. Note that tests using this annotation are
+ * expected to be run within an OSGi environment. It is a repeatable annotation and can
+ * be used to specify a number of different services using the {@link #value() service
+ * type} and an optional {@link #filter() LDAP filter expression}.
+ * <br>
+ * Supported parameter types ar the service type itself for mandatory and unary references (1..1),
+ * a {@code Collection} or {@code List} of the service type for optional and multiple references (0..n).
+ * Currently no other cardinalities are supported.
+ * <br>
+ * When used on a test class, the specified services are made available for injection as parameters
+ * to all of the test's methods.
+ * <br>
+ * When used on a test method, the specified services are made available for injection as parameters
+ * to exactly that method.
+ * <br>
+ * When used on a method parameter, the specified service is made available for injection for exactly
+ * that parameter. In this case, the {@link #value() service type} need not be specified, it can
+ * be inferred from the parameter's type. However, it may still be useful to specify a filter expression.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
+@Repeatable(Services.class)
+@Inherited
+public @interface Service {
+
+    /**
+     * The type of the service to be injected.
+     * <br>
+     * May be omitted if the annotation is used to annotate a method parameter, as the service type can
+     * be inferred from the parameter's type.
+     */
+    Class<?> value() default Object.class;
+
+    /**
+     * An optional filter expression conforming to the LDAP filter syntax used in OSGi {@link Filter}s.
+     */
+    String filter() default "";
+}
+
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/Services.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/Services.java
new file mode 100644
index 0000000..477c12f
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/Services.java
@@ -0,0 +1,36 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Utility annotation used to allow the {@link Service @Service} annotation to be repeatable.
+ * It is possible, but unnecessary, to use this annotation directly.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
+@Inherited
+public @interface Services {
+    Service[] value() default {};
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/AbstractTypeBasedParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/AbstractTypeBasedParameterResolver.java
new file mode 100644
index 0000000..c2817fe
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/AbstractTypeBasedParameterResolver.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi.impl;
+
+import org.jetbrains.annotations.NotNull;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
+
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.Map;
+
+import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.determineTypeArguments;
+
+/**
+ * Abstract {@link ParameterResolver} class that resolves any type-arguments in the parameter's type
+ * to their actual type and provides this {@code resolvedParameterType} to the abstract methods
+ * {@link #supportsParameter(ParameterContext, ExtensionContext, Type)} and
+ * {@link #resolveParameter(ParameterContext, ExtensionContext, Type)}.
+ */
+public abstract class AbstractTypeBasedParameterResolver implements ParameterResolver {
+
+    protected abstract boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+
+    protected abstract Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+
+    @Override
+    public final boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+        final Type type = getTypeOfParameter(parameterContext, extensionContext);
+        return supportsParameter(parameterContext, extensionContext, type);
+    }
+
+    @Override
+    public final Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+        final Type typeOfParameter = getTypeOfParameter(parameterContext, extensionContext);
+        return resolveParameter(parameterContext, extensionContext, typeOfParameter);
+    }
+
+    @NotNull
+    private static Type getTypeOfParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
+        Type type = parameterContext.getParameter().getParameterizedType();
+        if (type instanceof TypeVariable) {
+            final Map<TypeVariable<?>, Type> typeVariableTypeMap = determineTypeArguments(extensionContext.getRequiredTestClass());
+            return typeVariableTypeMap.getOrDefault((TypeVariable<?>) type, type);
+        } else {
+            return type;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleContextParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleContextParameterResolver.java
new file mode 100644
index 0000000..a77b747
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleContextParameterResolver.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi.impl;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+import java.lang.reflect.Type;
+
+public class BundleContextParameterResolver extends TypeBasedParameterResolver<BundleContext> {
+    @Override
+    protected BundleContext resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+        return FrameworkUtil.getBundle(extensionContext.getRequiredTestClass()).getBundleContext();
+    }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleParameterResolver.java
new file mode 100644
index 0000000..4bcc7e1
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/BundleParameterResolver.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi.impl;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.FrameworkUtil;
+
+import java.lang.reflect.Type;
+
+public class BundleParameterResolver extends TypeBasedParameterResolver<Bundle> {
+    @Override
+    protected Bundle resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+        return FrameworkUtil.getBundle(extensionContext.getRequiredTestClass());
+    }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ReflectionHelper.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ReflectionHelper.java
new file mode 100644
index 0000000..8ed7fe9
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ReflectionHelper.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi.impl;
+
+import org.apache.commons.lang3.reflect.TypeUtils;
+import org.jetbrains.annotations.NotNull;
+import org.junit.platform.commons.util.Preconditions;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class used for resolving type-arguments to concrete types.
+ */
+public class ReflectionHelper {
+
+    public static Map<TypeVariable<?>, Type> determineTypeArguments(@NotNull Class<?> clazz) {
+        final Map<TypeVariable<?>, Type> typeVariableTypeMap = new HashMap<>();
+        determineTypeArguments(clazz, typeVariableTypeMap);
+        return Collections.unmodifiableMap(typeVariableTypeMap);
+    }
+
+    private static void determineTypeArguments(Class<?> clazz, Map<TypeVariable<?>, Type> typeVariableTypeMap) {
+        final Type genericSuperclass = clazz.getGenericSuperclass();
+        if (genericSuperclass instanceof ParameterizedType) {
+            final ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
+            typeVariableTypeMap.putAll(TypeUtils.determineTypeArguments(clazz, parameterizedType));
+
+            final Type rawType = parameterizedType.getRawType();
+            if (!(rawType instanceof Class<?>)) {
+                throw new UnsupportedOperationException("Expected Class#getGenericSuperclass() to return an object of type Class<?>");
+            }
+            determineTypeArguments((Class<?>) rawType, typeVariableTypeMap);
+        } else if (genericSuperclass instanceof Class<?>) {
+            if (genericSuperclass == Object.class && ((Class<?>) genericSuperclass).isArray()) {
+                // TODO: handle array types, see docs for Class#getGenericSuperclass()
+                throw new UnsupportedOperationException("Unsupported case where genericSuperclass == Object.class");
+            } else {
+                determineTypeArguments((Class<?>) genericSuperclass, typeVariableTypeMap);
+            }
+        } else if (genericSuperclass == null) {
+            final Type[] genericInterfaces = clazz.getGenericInterfaces();
+            for (Type genericInterface : genericInterfaces) {
+                if (genericInterface instanceof ParameterizedType) {
+                    final ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
+                    typeVariableTypeMap.putAll(TypeUtils.determineTypeArguments(clazz, parameterizedType));
+                }
+            }
+        } else {
+            throw new UnsupportedOperationException("Expected Class#getGenericSuperclass() to return null or an object of type Class<?> or ParameterizedType");
+        }
+    }
+
+
+    @NotNull
+    public static ParameterizedType parameterizedTypeForBaseClass(@NotNull Class<?> baseClass, @NotNull Class<?> clazz) {
+        ParameterizedType parameterizedType = findParameterizedTypeForBaseClass(baseClass, clazz);
+        Preconditions.notNull(parameterizedType,
+                () -> String.format(
+                        "Failed to discover type supported by %s; "
+                                + "potentially caused by lacking parameterized type in class declaration.",
+                        clazz.getName()));
+        return parameterizedType;
+    }
+
+    private static ParameterizedType findParameterizedTypeForBaseClass(Class<?> baseClass, Class<?> clazz) {
+        Class<?> superclass = clazz.getSuperclass();
+
+        // Abort?
+        if (superclass == null || superclass == Object.class) {
+            return null;
+        }
+
+        Type genericSuperclass = clazz.getGenericSuperclass();
+        if (genericSuperclass instanceof ParameterizedType) {
+            Type rawType = ((ParameterizedType) genericSuperclass).getRawType();
+            if (rawType == baseClass) {
+                return ((ParameterizedType) genericSuperclass);
+            }
+        }
+        return findParameterizedTypeForBaseClass(baseClass, superclass);
+    }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ServiceParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ServiceParameterResolver.java
new file mode 100644
index 0000000..05a2af2
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/ServiceParameterResolver.java
@@ -0,0 +1,263 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi.impl;
+
+import org.apache.sling.junit.jupiter.osgi.Service;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.platform.commons.support.AnnotationSupport;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.util.tracker.ServiceTracker;
+
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+import static org.junit.platform.commons.support.AnnotationSupport.findAnnotation;
+
+public class ServiceParameterResolver extends AbstractTypeBasedParameterResolver {
+
+    private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(ServiceParameterResolver.class);
+
+    @Override
+    protected boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+        return computeServiceType(resolvedParameterType)
+                .flatMap(serviceType -> findServiceAnnotation(parameterContext, extensionContext, serviceType))
+                .isPresent();
+    }
+
+    @Override
+    protected Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+        return computeServiceType(resolvedParameterType)
+                .map(serviceType -> {
+                    final Optional<Service> serviceAnnotation = findServiceAnnotation(parameterContext, extensionContext, serviceType);
+                    return serviceAnnotation
+                            .map(ann -> toKey(serviceType, ann))
+                            .map(key -> extensionContext.getStore(NAMESPACE)
+                                    .getOrComputeIfAbsent(key, serviceHolderFactory(extensionContext, serviceType), ServiceHolder.class))
+                            .map(serviceHolder -> isMultiple(resolvedParameterType) ? serviceHolder.getServices() : serviceHolder.getService())
+                            .orElseThrow(() -> createServiceNotFoundException(serviceAnnotation.map(Service::filter).orElse(null), serviceType));
+                })
+                .orElseThrow(() -> new ParameterResolutionException("Cannot handle type " + resolvedParameterType));
+    }
+
+    private static ServiceHolder.Key toKey(Class<?> serviceType, Service serviceAnnotation) {
+        return new ServiceHolder.Key(serviceType, serviceAnnotation);
+    }
+
+    @NotNull
+    private static Optional<Class<?>> computeServiceType(Type resolvedParameterType) {
+        if (resolvedParameterType instanceof ParameterizedType) {
+            final ParameterizedType parameterizedType = (ParameterizedType) resolvedParameterType;
+            final Class<?> clazz = getRawClass(parameterizedType);
+            if (Collection.class == clazz || List.class.isAssignableFrom(clazz)) {
+                final Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
+                if (actualTypeArguments.length == 1 && actualTypeArguments[0] instanceof Class<?>) {
+                    return Optional.of((Class<?>) actualTypeArguments[0]);
+                }
+            }
+        } else if (resolvedParameterType instanceof Class<?>) {
+            return Optional.of((Class<?>) resolvedParameterType);
+        }
+        return Optional.empty();
+    }
+
+    @NotNull
+    private static Class<?> getRawClass(ParameterizedType parameterizedType) {
+        final Type rawType = parameterizedType.getRawType();
+        if (!(rawType instanceof Class<?>)) {
+            throw new UnsupportedOperationException("Unexpected raw type of parametereized type " + parameterizedType + ": " + rawType);
+        }
+        return (Class<?>) rawType;
+    }
+
+    private ParameterResolutionException createServiceNotFoundException(String ldapFilter, Type resolvedParameterType) {
+        return Optional.ofNullable(ldapFilter)
+                .map(String::trim)
+                .filter(filter -> !filter.isEmpty())
+                .map(filter -> new ParameterResolutionException("No service of type " + resolvedParameterType + " with filter \"" + filter + "\" available"))
+                .orElseGet(() -> new ParameterResolutionException("No service of type " + resolvedParameterType + " available"));
+    }
+
+    @NotNull
+    private static Function<ServiceHolder.Key, ServiceHolder> serviceHolderFactory(ExtensionContext extensionContext, Class<?> requiredServiceType) {
+        return key -> new ServiceHolder(getBundleContext(extensionContext), key);
+    }
+
+    @Nullable
+    private static BundleContext getBundleContext(ExtensionContext extensionContext) {
+        return Optional.ofNullable(FrameworkUtil.getBundle(extensionContext.getRequiredTestClass()))
+                .map(Bundle::getBundleContext)
+                .orElse(null);
+    }
+
+    @NotNull
+    private static Optional<Service> findServiceAnnotation(ParameterContext parameterContext, ExtensionContext extensionContext, Class<?> requiredServiceType) {
+        return Stream.<Supplier<Optional<Service>>>of(
+                () -> findServiceAnnotationOnParameter(parameterContext, requiredServiceType),
+                () -> findServiceAnnotationOnMethodOrClass(extensionContext, requiredServiceType))
+                .map(Supplier::get)
+                .filter(Optional::isPresent)
+                .map(Optional::get)
+                .findFirst();
+    }
+
+    private static Optional<Service> findServiceAnnotationOnMethodOrClass(ExtensionContext extensionContext, Class<?> requiredServiceType) {
+        return extensionContext.getElement()
+                .map(ae -> findMatchingServiceAnnotation(ae, requiredServiceType))
+                .filter(Optional::isPresent)
+                .orElseGet(() -> extensionContext.getParent().flatMap(p -> findServiceAnnotationOnMethodOrClass(p, requiredServiceType)));
+    }
+
+    private static Optional<Service> findServiceAnnotationOnParameter(ParameterContext parameterContext, Class<?> requiredServiceType) {
+        final Optional<Service> serviceAnnotation = findAnnotation(parameterContext.getParameter(), Service.class);
+        serviceAnnotation.ifPresent(ann -> {
+            if (!ann.value().isAssignableFrom(requiredServiceType)) {
+                throw new ParameterResolutionException("Mismatched types in annotation and parameter. " +
+                        "Annotation type is \"" + ann.value().getSimpleName() + "\", parameter type is \"" + requiredServiceType.getSimpleName() + "\"");
+            }
+        });
+        return serviceAnnotation;
+    }
+
+    private static Optional<Service> findMatchingServiceAnnotation(AnnotatedElement annotatedElement, Class<?> requiredServiceType) {
+        return AnnotationSupport.findRepeatableAnnotations(annotatedElement, Service.class)
+                .stream()
+                .filter(serviceAnnotation -> Objects.equals(serviceAnnotation.value(), requiredServiceType))
+                .findFirst();
+    }
+
+    private boolean isMultiple(Type resolvedParameterType) {
+        if (resolvedParameterType instanceof ParameterizedType) {
+            final Class<?> type = getRawClass((ParameterizedType) resolvedParameterType);
+            return Collection.class == type || List.class.isAssignableFrom(type);
+        }
+        return false;
+    }
+
+    private static class ServiceHolder implements ExtensionContext.Store.CloseableResource {
+
+        private final ServiceTracker<?, ?> serviceTracker;
+
+        private ServiceHolder(BundleContext bundleContext, Key key) {
+            final Filter filter = createFilter(bundleContext, key.type(), key.filter());
+            serviceTracker = new SortingServiceTracker<>(bundleContext, filter);
+            serviceTracker.open();
+        }
+
+        @Override
+        public void close() throws Throwable {
+            serviceTracker.close();
+        }
+
+        public Object getService() {
+            return serviceTracker.getService();
+        }
+
+        public List<Object> getServices() {
+            final Object[] services = serviceTracker.getServices();
+            return services == null ? Collections.emptyList() : Arrays.asList(services);
+        }
+
+        private static Filter createFilter(BundleContext bundleContext, Class<?> clazz, String ldapFilter) {
+            final String classFilter = String.format("(%s=%s)", Constants.OBJECTCLASS, clazz.getName());
+            final String combinedFilter;
+            if (ldapFilter == null || ldapFilter.trim().isEmpty()) {
+                combinedFilter = classFilter;
+            } else {
+                combinedFilter = String.format("(&%s%s)", classFilter, ldapFilter);
+            }
+            try {
+                return bundleContext.createFilter(combinedFilter);
+            } catch (InvalidSyntaxException e) {
+                throw new IllegalArgumentException("Invalid filter expression: \"" + ldapFilter + "\"", e);
+            }
+        }
+
+        private static class SortingServiceTracker<T> extends ServiceTracker<T, T> {
+            public SortingServiceTracker(BundleContext bundleContext, Filter filter) {
+                super(bundleContext, filter, null);
+            }
+
+            @Override
+            public ServiceReference<T>[] getServiceReferences() {
+                return Optional.ofNullable(super.getServiceReferences())
+                        .map(serviceReferences -> {
+                            Arrays.sort(serviceReferences);
+                            return serviceReferences;
+                        })
+                        .orElse(null);
+            }
+        }
+
+        private static class Key {
+
+            private final Class<?> serviceType;
+
+            private final Service serviceAnnotation;
+
+            public Key(Class<?> serviceType, Service serviceAnnotation) {
+                this.serviceType = serviceType;
+                this.serviceAnnotation = serviceAnnotation;
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                if (!(o instanceof Key)) {
+                    return false;
+                }
+                Key key = (Key) o;
+                return this == o
+                        || (Objects.equals(serviceType, key.serviceType)
+                        && Objects.equals(serviceAnnotation, key.serviceAnnotation));
+            }
+
+            @Override
+            public int hashCode() {
+                return Objects.hash(serviceType, serviceAnnotation);
+            }
+
+            public Class<?> type() {
+                return serviceType;
+            }
+
+            public String filter() {
+                return serviceAnnotation.filter();
+            }
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/TypeBasedParameterResolver.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/TypeBasedParameterResolver.java
new file mode 100644
index 0000000..a8ec0d2
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/impl/TypeBasedParameterResolver.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi.impl;
+
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.parameterizedTypeForBaseClass;
+
+/**
+ * Abstract implementation of a {@link org.junit.jupiter.api.extension.ParameterResolver} that resolves
+ * parameters of one given type. Implementations need only implement the abstract method
+ * {@link #resolveParameter(ParameterContext, ExtensionContext, Type)}, the supported parameter type is
+ * inferred from the classes type-argument {@code T}.
+ *
+ * @param <T>
+ */
+public abstract class TypeBasedParameterResolver<T> extends AbstractTypeBasedParameterResolver {
+
+    private final Type supportedType;
+
+    public TypeBasedParameterResolver() {
+        ParameterizedType parameterizedType = parameterizedTypeForBaseClass(TypeBasedParameterResolver.class, getClass());
+        this.supportedType = parameterizedType.getActualTypeArguments()[0];
+    }
+
+    @Override
+    protected boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType) {
+        return supportedType == resolvedParameterType;
+    }
+
+    @Override
+    protected abstract T resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext, Type resolvedParameterType);
+}
diff --git a/src/main/java/org/apache/sling/junit/jupiter/osgi/package-info.java b/src/main/java/org/apache/sling/junit/jupiter/osgi/package-info.java
new file mode 100644
index 0000000..83d36bf
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/package-info.java
@@ -0,0 +1,22 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+@Version("1.0.0")
+package org.apache.sling.junit.jupiter.osgi;
+
+import org.osgi.annotation.versioning.Version;
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/junit/impl/servlet/HtmlRendererTest.java b/src/test/java/org/apache/sling/junit/impl/servlet/HtmlRendererTest.java
index dd86d3d..c175746 100644
--- a/src/test/java/org/apache/sling/junit/impl/servlet/HtmlRendererTest.java
+++ b/src/test/java/org/apache/sling/junit/impl/servlet/HtmlRendererTest.java
@@ -18,7 +18,8 @@
  */
 package org.apache.sling.junit.impl.servlet;
 
-import org.apache.sling.junit.impl.servlet.junit5.JUnit5TestExecutionStrategy;
+import org.apache.sling.junit.impl.servlet.junit5.JUnitPlatformHelper;
+import org.apache.sling.junit.impl.servlet.junit5.RunListenerAdapter;
 import org.hamcrest.Matchers;
 import org.junit.Assert;
 import org.junit.Assume;
@@ -29,9 +30,6 @@ import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
 import org.junit.platform.engine.TestEngine;
-import org.junit.platform.launcher.Launcher;
-import org.junit.platform.launcher.LauncherDiscoveryRequest;
-import org.junit.runner.notification.RunListener;
 import org.junit.vintage.engine.VintageTestEngine;
 
 import java.io.PrintWriter;
@@ -86,18 +84,10 @@ public class HtmlRendererTest {
         final StringWriter out = new StringWriter();
         final HtmlRenderer htmlRenderer = new HtmlRenderer();
         htmlRenderer.setWriter(new PrintWriter(out));
-        runTest(testEngine, htmlRenderer, testClass, methodName);
+        JUnitPlatformHelper.executeTest(testEngine, testClass, methodName, new RunListenerAdapter(htmlRenderer));
         return out.toString();
     }
 
-    private static void runTest(TestEngine testEngine, RunListener runListener, Class<?> testClass, String methodName) {
-        final Launcher launcher = JUnit5TestExecutionStrategy.createLauncher(runListener, testEngine);
-        final LauncherDiscoveryRequest request = methodName != null
-                ? JUnit5TestExecutionStrategy.methodRequest(testClass, methodName)
-                : JUnit5TestExecutionStrategy.classesRequest(testClass);
-        launcher.execute(request);
-    }
-
     public static class ExampleTestCases {
 
         public static final String ASSUMPTION_IS_ALWAYS_INVALID = "Assumption is always invalid";
diff --git a/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java b/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java
new file mode 100644
index 0000000..e315622
--- /dev/null
+++ b/src/test/java/org/apache/sling/junit/jupiter/osgi/OSGiAnnotationTest.java
@@ -0,0 +1,392 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.junit.jupiter.osgi;
+
+import org.apache.sling.junit.impl.servlet.junit5.JUnitPlatformHelper;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContext;
+import org.apache.sling.testing.mock.osgi.junit5.OsgiContextExtension;
+import org.hamcrest.Matchers;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.engine.JupiterTestEngine;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.platform.commons.annotation.Testable;
+import org.junit.platform.commons.support.HierarchyTraversalMode;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.opentest4j.MultipleFailuresError;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.util.Collection;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static java.util.Arrays.asList;
+import static org.apache.sling.junit.jupiter.osgi.impl.ReflectionHelper.parameterizedTypeForBaseClass;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.instanceOf;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedMethods;
+
+/**
+ * This test executes pseudo test classes using the {@link JupiterTestEngine} in order to
+ * verify the correct injection of parameters via the {@code @OSGi} and  {@code @Service}
+ * annotations.
+ * <br>
+ * In order to achieve this, test methods from the pseudo test classes ({@code PseudoTest*}
+ * are executed and the test summary evaluated to verify expectations. The additional indirection
+ * can be a little tricky, but is necessary to test the annotations work correctly. Particularly
+ * when testing failure scenarios, where the failure of a pseudo test is required to pass the actual
+ * test.
+ */
+@ExtendWith(OsgiContextExtension.class)
+public class OSGiAnnotationTest {
+
+    private static final JupiterTestEngine JUPITER_TEST_ENGINE = new JupiterTestEngine();
+
+    OsgiContext osgiContext = new OsgiContext();
+
+    @SuppressWarnings("unused") // provides parameters
+    static Stream<Arguments> frameworkObjectsInjectionTests() {
+        return Stream.of(PseudoTestBundleInjection.class, PseudoTestBundleContextInjection.class)
+                .flatMap(OSGiAnnotationTest::allTestMethods);
+    }
+
+    @ParameterizedTest(name = "{0}#{2}")
+    @MethodSource("frameworkObjectsInjectionTests")
+    void injectFrameworkObjects(String name, Class<?> testClass, String testMethodName) {
+        withMockedFrameworkUtil(() -> {
+            assertNoFailures(testClass, testMethodName);
+        });
+    }
+
+    @SuppressWarnings("unused") // provides parameters
+    static Stream<Arguments> serviceInjectionTests() {
+        return Stream
+                .of(
+                        PseudoTestServiceInjectionGloballyAnnotated.class,
+                        PseudoTestServiceInjectionGloballyAnnotatedWithFilter.class,
+                        PseudoTestInheritedServiceInjectionGloballyAnnotated.class)
+                .flatMap(OSGiAnnotationTest::allTestMethods);
+    }
+
+    @ParameterizedTest(name = "{0}#{2}")
+    @MethodSource("serviceInjectionTests")
+    void injectServices(String name, Class<?> testClass, String testMethodName) {
+        osgiContext.registerService(ServiceInterface.class, new ServiceA(), "foo", "quz");
+        withMockedFrameworkUtil(() -> {
+            assertNoFailures(testClass, testMethodName);
+        });
+    }
+
+    @SuppressWarnings("unused") // provides parameters
+    static Stream<Arguments> failConstructionDueToMissingServiceInjectionTests() {
+        return Stream.of(PseudoTestServiceInjectionNotAnnotated.class, PseudoTestServiceInjectionGloballyAnnotatedWithFilter.class)
+                .flatMap(namedMethods("injectedConstructorParameter"));
+    }
+
+    @ParameterizedTest(name = "{0}#{2}")
+    @MethodSource("failConstructionDueToMissingServiceInjectionTests")
+    void failConstructionDueToMissingServiceInjection(String name, Class<?> testClass, String testMethodName) {
+        // setup service with non-matching filter
+        osgiContext.registerService(ServiceInterface.class, new ServiceA(), "foo", "no match");
+        withMockedFrameworkUtil(() -> {
+            assertTestConstructionFailsDueToMissingService(testClass, testMethodName);
+        });
+    }
+
+    @Test
+    void injectServiceAsAnnotatedMethodParameterWithExplicitClass() {
+        osgiContext.registerService(ServiceInterface.class, new ServiceA());
+        withMockedFrameworkUtil(() -> {
+            assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithExplicitClass");
+        });
+    }
+
+    @Test
+    void injectServiceAsAnnotatedMethodParameterWithImplicitClass() {
+        osgiContext.registerService(ServiceInterface.class, new ServiceA());
+        withMockedFrameworkUtil(() -> {
+            assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithImpliedClass");
+        });
+    }
+
+    @Test
+    void injectServiceAsAnnotatedMethodParameterWithExplicitClassMultiple() {
+        osgiContext.registerService(ServiceInterface.class, new ServiceA(), "service.ranking", 1);
+        osgiContext.registerService(ServiceInterface.class, new ServiceC(), "service.ranking", 3);
+        osgiContext.registerService(ServiceInterface.class, new ServiceB(), "service.ranking", 2);
+        withMockedFrameworkUtil(() -> {
+            assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithExplicitClassMultiple");
+        });
+    }
+
+    @Test
+    void injectServiceAsAnnotatedMethodParameterWithImplicitClassMultiple() {
+        osgiContext.registerService(ServiceInterface.class, new ServiceA(), "service.ranking", 1);
+        osgiContext.registerService(ServiceInterface.class, new ServiceC(), "service.ranking", 3);
+        osgiContext.registerService(ServiceInterface.class, new ServiceB(), "service.ranking", 2);
+        withMockedFrameworkUtil(() -> {
+            assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithImpliedClassMultiple");
+        });
+    }
+
+
+    @Test
+    void injectServiceAsAnnotatedMethodParameterWithImplicitClassEmptyMultiple() {
+        withMockedFrameworkUtil(() -> {
+            assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedParameterWithImpliedClassEmptyMultiple");
+        });
+    }
+
+    @Test
+    void injectServiceAsAnnotatedMethodParameterWithIncorrectExplicitClassMultiple() {
+        osgiContext.registerService(ServiceInterface.class, new ServiceA(), "service.ranking", 1);
+        osgiContext.registerService(ServiceInterface.class, new ServiceC(), "service.ranking", 3);
+        osgiContext.registerService(ServiceInterface.class, new ServiceB(), "service.ranking", 2);
+        withMockedFrameworkUtil(() -> {
+            final TestExecutionSummary summary = executeAndSummarize(PseudoTestServiceMethodInjection.class, "annotatedParameterWithIncorrectExplicitClassMultiple");
+            assertEquals(1, summary.getTestsFailedCount(), "expected test failure count");
+            final Throwable exception = summary.getFailures().get(0).getException();
+            assertThat(exception, instanceOf(ParameterResolutionException.class));
+            assertThat(exception.getMessage(), equalTo("Mismatched types in annotation and parameter. " +
+                    "Annotation type is \"ServiceB\", parameter type is \"ServiceInterface\""));
+        });
+    }
+
+    @Test
+    void injectServiceAsParameterOfAnnotatedMethod() {
+        osgiContext.registerService(ServiceInterface.class, new ServiceA());
+        withMockedFrameworkUtil(() -> {
+            assertNoFailures(PseudoTestServiceMethodInjection.class, "annotatedMethod");
+        });
+    }
+
+    @OSGi
+    static class PseudoTestServiceMethodInjection {
+        @Test
+        void annotatedParameterWithExplicitClass(@Service(ServiceInterface.class) ServiceInterface serviceA) {
+            assertThat(serviceA, instanceOf(ServiceA.class));
+        }
+
+        @Test
+        void annotatedParameterWithImpliedClass(@Service ServiceInterface serviceA) {
+            assertThat(serviceA, instanceOf(ServiceA.class));
+        }
+
+        @Test
+        void annotatedParameterWithExplicitClassMultiple(@Service(ServiceInterface.class) List<ServiceInterface> services) {
+            assertThat(services, instanceOf(List.class));
+            assertThat(services, contains(asList(instanceOf(ServiceA.class), instanceOf(ServiceB.class), instanceOf(ServiceC.class))));
+        }
+
+        @Test
+        void annotatedParameterWithImpliedClassMultiple(@Service Collection<ServiceInterface> services) {
+            assertThat(services, instanceOf(Collection.class));
+            assertThat(services, contains(asList(instanceOf(ServiceA.class), instanceOf(ServiceB.class), instanceOf(ServiceC.class))));
+        }
+
+        @Test
+        void annotatedParameterWithImpliedClassEmptyMultiple(@Service List<ServiceInterface> services) {
+            assertThat(services, instanceOf(List.class));
+            assertThat(services, empty());
+        }
+
+        @Test
+        void annotatedParameterWithIncorrectExplicitClassMultiple(@Service(ServiceB.class) List<ServiceInterface> services) {
+            assertThat(services, instanceOf(List.class));
+            assertThat(services, contains(instanceOf(ServiceA.class)));
+        }
+
+        @Test
+        @Service(ServiceInterface.class)
+        void annotatedMethod(ServiceInterface serviceA) {
+            assertThat(serviceA, instanceOf(ServiceA.class));
+        }
+    }
+
+    private void withMockedFrameworkUtil(Runnable callback) {
+        try (final MockedStatic<FrameworkUtil> frameworkUtilMock = Mockito.mockStatic(FrameworkUtil.class)) {
+            frameworkUtilMock
+                    .when(() -> FrameworkUtil.getBundle(Mockito.any()))
+                    .then(invocation -> osgiContext.bundleContext().getBundle());
+            callback.run();
+        }
+    }
+
+    @NotNull
+    private static Stream<Arguments> allTestMethods(Class<?> cls) {
+        return findAnnotatedMethods(cls, Testable.class, HierarchyTraversalMode.BOTTOM_UP).stream()
+                .map(toArguments(cls));
+    }
+
+    @NotNull
+    private static Function<Class<?>, Stream<Arguments>> namedMethods(String... testMethodNames) {
+        return cls -> findAnnotatedMethods(cls, Testable.class, HierarchyTraversalMode.BOTTOM_UP).stream()
+                .filter(method -> asList(testMethodNames).contains(method.getName()))
+                .map(toArguments(cls));
+    }
+
+    @NotNull
+    private static Function<Method, Arguments> toArguments(Class<?> cls) {
+        return method -> Arguments.of(cls.getSimpleName(), cls, method.getName());
+    }
+
+    private static TestExecutionSummary executeAndSummarize(@NotNull Class<?> testClass, @Nullable String testMethodName) {
+        final SummaryGeneratingListener listener = new SummaryGeneratingListener();
+        JUnitPlatformHelper.executeTest(JUPITER_TEST_ENGINE, testClass, testMethodName, listener);
+        return listener.getSummary();
+    }
+
+    private static void assertNoFailures(@NotNull Class<?> testClass, @Nullable String testMethodName) {
+        final TestExecutionSummary summary = executeAndSummarize(testClass, testMethodName);
+        assertThat("number of tests found", (int) summary.getTestsFoundCount(), greaterThan(0));
+        final List<TestExecutionSummary.Failure> failures = summary.getFailures();
+        switch (failures.size()) {
+            case 0:
+                break;
+            case 1:
+                fail(failures.get(0).getException());
+            default:
+                throw new MultipleFailuresError(null, failures.stream().map(TestExecutionSummary.Failure::getException).collect(Collectors.toList()));
+        }
+    }
+
+    private void assertTestConstructionFailsDueToMissingService(Class<?> testClass, String testMethodName) {
+        final TestExecutionSummary summary = executeAndSummarize(testClass, testMethodName);
+        final List<TestExecutionSummary.Failure> failures = summary.getFailures();
+        assertEquals(1, failures.size(), "number of test failures");
+        final TestExecutionSummary.Failure failure = failures.get(0);
+        final Throwable exception = failure.getException();
+        assertThat(exception, Matchers.instanceOf(ParameterResolutionException.class));
+        assertThat(exception.getMessage(), anyOf(
+                allOf(containsString("No ParameterResolver registered for parameter "), containsString(" in constructor ")),
+                // allOf(containsString("Failed to resolve parameter "), containsString(" in constructor ")),
+                allOf(containsString("No service of type "), containsString(" available"))
+        ));
+    }
+
+    @OSGi
+    static class PseudoTestBundleContextInjection extends Injection<BundleContext> {
+        public PseudoTestBundleContextInjection(BundleContext object) {
+            super(object);
+        }
+    }
+
+    @OSGi
+    static class PseudoTestBundleInjection extends Injection<Bundle> {
+        public PseudoTestBundleInjection(Bundle object) {
+            super(object);
+        }
+    }
+
+    @OSGi
+    static class PseudoTestServiceInjectionNotAnnotated extends Injection<ServiceInterface> {
+        public PseudoTestServiceInjectionNotAnnotated(ServiceInterface object) {
+            super(object);
+        }
+
+        @Override
+        void injectedMethodParameter(ServiceInterface objectFromMethodInjection) {
+            super.injectedMethodParameter(objectFromMethodInjection);
+        }
+    }
+
+    @OSGi
+    @Service(ServiceInterface.class)
+    static class PseudoTestServiceInjectionGloballyAnnotated extends Injection<ServiceInterface> {
+        public PseudoTestServiceInjectionGloballyAnnotated(ServiceInterface object) {
+            super(object);
+        }
+    }
+
+    static class PseudoTestInheritedServiceInjectionGloballyAnnotated extends PseudoTestServiceInjectionGloballyAnnotated {
+        public PseudoTestInheritedServiceInjectionGloballyAnnotated(ServiceInterface object) {
+            super(object);
+        }
+    }
+
+    @OSGi
+    @Service(value = ServiceInterface.class, filter = "(foo=quz)")
+    static class PseudoTestServiceInjectionGloballyAnnotatedWithFilter extends Injection<ServiceInterface> {
+        public PseudoTestServiceInjectionGloballyAnnotatedWithFilter(ServiceInterface object) {
+            super(object);
+        }
+    }
+
+    static abstract class Injection<T> {
+
+        T objectFromConstructor;
+
+        private final String typeName;
+
+        public Injection(T object) {
+            this.objectFromConstructor = object;
+            final ParameterizedType parameterizedType = parameterizedTypeForBaseClass(Injection.class, getClass());
+            this.typeName = ((Class<?>) parameterizedType.getActualTypeArguments()[0]).getSimpleName();
+        }
+
+        @Test
+        final void injectedConstructorParameter() {
+            assertNotNull(objectFromConstructor, typeName + " constructor parameter");
+        }
+
+        @Test
+        void injectedMethodParameter(T objectFromMethodInjection) {
+            assertNotNull(objectFromMethodInjection, typeName + " method parameter");
+            assertSame(objectFromConstructor, objectFromMethodInjection,
+                    typeName + " same parameter should be injected into method and constructor");
+        }
+    }
+
+    interface ServiceInterface {
+    }
+
+    static class ServiceA implements ServiceInterface {
+    }
+
+    static class ServiceB implements ServiceInterface {
+    }
+
+    static class ServiceC implements ServiceInterface {
+    }
+}