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/17 14:12:42 UTC

[sling-org-apache-sling-junit-core] branch feature/SLING-10497-junit-jupiter-parameter-resolver-for-osgi updated (852ed28 -> 15a5703)

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

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


 discard 852ed28  SLING-10497 - JUnit Jupiter ParameterResolver for OSGi
     new 15a5703  SLING-10497 - JUnit Jupiter ParameterResolver for OSGi

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (852ed28)
            \
             N -- N -- N   refs/heads/feature/SLING-10497-junit-jupiter-parameter-resolver-for-osgi (15a5703)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

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

Posted by js...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

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

    SLING-10497 - JUnit Jupiter ParameterResolver for OSGi
---
 bnd.bnd                                            |   3 +
 pom.xml                                            |  25 +-
 .../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   |  70 ++++
 .../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, 1271 insertions(+), 59 deletions(-)

diff --git a/bnd.bnd b/bnd.bnd
index aea6f89..a60cacf 100644
--- a/bnd.bnd
+++ b/bnd.bnd
@@ -1,8 +1,11 @@
 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, \
+                org.apache.commons.lang3.reflect.*;resolution:=optional, \
                 *
 -includeresource: @org.jacoco.agent-*.jar!/org/jacoco/agent/rt/IAgent*
diff --git a/pom.xml b/pom.xml
index 9e68ab1..e5f2f36 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,9 +37,10 @@
         <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.6.0</junit-jupiter.version>
         <maven.compiler.source>1.8</maven.compiler.source>
         <maven.compiler.target>1.8</maven.compiler.target>
+        <sonar.test.exclusions>**/*PseudoTest*</sonar.test.exclusions>
     </properties>
 
     <scm>
@@ -282,7 +283,14 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.commons.osgi</artifactId>
-            <version>2.2.2</version>
+            <version>2.4.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 -->
@@ -319,7 +327,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 +353,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..3384a37
--- /dev/null
+++ b/src/main/java/org/apache/sling/junit/jupiter/osgi/Service.java
@@ -0,0 +1,70 @@
+/*
+ * 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;
+
+/**
+ * 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 {
+    }
+}