Skip to content

Commit

Permalink
Introduce InvocationInterceptor extension API
Browse files Browse the repository at this point in the history
The new extension API allows intercepting the invocation of test class
constructors, lifecycle methods, testable methods, and dynamic tests.
It validates that an invocation is asked to proceed exactly once. The
user guide is updated with an example that executes all test methods in
Swing's EDT.

Resolves #157.
  • Loading branch information
marcphilipp committed Apr 8, 2019
1 parent b1c66e4 commit 8bb09ad
Show file tree
Hide file tree
Showing 23 changed files with 1,150 additions and 102 deletions.
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ endif::[]
:ExtendWith: {javadoc-root}/org/junit/jupiter/api/extension/ExtendWith.html[@ExtendWith]
:ExtensionContext: {javadoc-root}/org/junit/jupiter/api/extension/ExtensionContext.html[ExtensionContext]
:ExtensionContext_Store: {javadoc-root}/org/junit/jupiter/api/extension/ExtensionContext.Store.html[Store]
:InvocationInterceptor: {javadoc-root}/org/junit/jupiter/api/extension/InvocationInterceptor[InvocationInterceptor]
:ParameterResolver: {javadoc-root}/org/junit/jupiter/api/extension/ParameterResolver.html[ParameterResolver]
:RegisterExtension: {javadoc-root}/org/junit/jupiter/api/extension/RegisterExtension.html[@RegisterExtension]
:TestExecutionExceptionHandler: {javadoc-root}/org/junit/jupiter/api/extension/TestExecutionExceptionHandler.html[TestExecutionExceptionHandler]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ on GitHub.
the log in order to allow reproducible builds.
* Methods ordered with `MethodOrderer.Random` now execute using the `SAME_THREAD`
concurrency mode instead of the `CONCURRENT` mode when no custom seed is provided.
* New `InvocationInterceptor` extension API (see
<<../user-guide/index.adoc#extensions-intercepting-invocations, User Guide>> for
details)


[[release-notes-5.5.0-M2-junit-vintage]]
Expand Down
18 changes: 18 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,21 @@ but rethrow any other type of exception.
include::{testDir}/example/exception/IgnoreIOExceptionExtension.java[tags=user_guide]
----

[[extensions-intercepting-invocations]]
=== Intercepting Invocations

`{InvocationInterceptor}` defines the API for `Extensions` that wish to intercept calls to
test code.

The following example shows an extension that executes all test methods in Swing's Event
Dispatch Thread.

[source,java,indent=0]
.An extension that executes tests in a user-defined thread
----
include::{testDir}/example/interceptor/SwingEdtInterceptor.java[tags=user_guide]
----

[[extensions-test-templates]]
=== Providing Invocation Contexts for Test Templates

Expand Down Expand Up @@ -631,6 +646,9 @@ steps are optional depending on the presence of user code or extension support f
corresponding lifecycle callback. For further details on the various lifecycle callbacks
please consult the respective Javadoc for each annotation and extension.

All invocations of user code methods in the above table can additionally be intercepted
using by implementing <<extensions-intercepting-invocations, `InvocationInterceptor`>>.

[[extensions-execution-order-wrapping-behavior]]
==== Wrapping Behavior of Callbacks

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* Copyright 2015-2019 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package example.interceptor;

import java.lang.reflect.Method;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.SwingUtilities;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.InvocationInterceptor;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;

// @formatter:off
// tag::user_guide[]
public class SwingEdtInterceptor implements InvocationInterceptor {

@Override
public void interceptTestMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext extensionContext) throws Throwable {
AtomicReference<Throwable> throwable = new AtomicReference<>();
SwingUtilities.invokeAndWait(() -> {
try {
invocation.proceed();
}
catch (Throwable t) {
throwable.set(t);
}
});
Throwable t = throwable.get();
if (t != null) {
throw t;
}
}
}
// end::user_guide[]
// @formatter:on
5 changes: 2 additions & 3 deletions gradle/testing.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,14 @@ tasks.named<Test>("test").configure {
}

dependencies {
"testImplementation"(project(":junit-jupiter-api"))
"testImplementation"(project(":junit-jupiter-params"))
"testImplementation"("org.assertj:assertj-core:${Versions.assertJ}")
"testImplementation"("org.mockito:mockito-junit-jupiter:${Versions.mockito}") {
exclude(module = "junit-jupiter-engine")
}

if (project.name != "junit-jupiter-engine") {
"testImplementation"(project(":junit-jupiter-api"))
"testImplementation"(project(":junit-jupiter-params"))

"testRuntimeOnly"(project(":junit-jupiter-engine"))
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright 2015-2019 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import org.apiguardian.api.API;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.TestTemplate;

/**
* {@code InvocationInterceptor} defines the API for {@link Extension
* Extensions} that wish to intercept calls to test code.
*
* <h3>Invocation Contract</h3>
*
* <p>Each method in this class must execute the passed {@link Invocation}
* exactly once. Otherwise, the enclosing test or container will be reported as
* failed.
*
* <h3>Constructor Requirements</h3>
*
* <p>Consult the documentation in {@link Extension} for details on
* constructor requirements.
*
* @since 5.5
* @see Invocation
* @see ReflectiveInvocationContext
* @see ExtensionContext
*/
@API(status = EXPERIMENTAL, since = "5.5")
public interface InvocationInterceptor extends Extension {

/**
* Intercept the invocation of a test class constructor.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @param <T> the result type
* @return the result of the invocation; never {@code null}
* @throws Throwable in case of failures
*/
default <T> T interceptTestClassConstructor(Invocation<T> invocation,
ReflectiveInvocationContext<Constructor<T>> invocationContext, ExtensionContext extensionContext)
throws Throwable {
return invocation.proceed();
}

/**
* Intercept the invocation of a {@link BeforeAll @BeforeAll} method.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @throws Throwable in case of failures
*/
default void interceptBeforeAllMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
invocation.proceed();
}

/**
* Intercept the invocation of a {@link BeforeEach @BeforeEach} method.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @throws Throwable in case of failures
*/
default void interceptBeforeEachMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
invocation.proceed();
}

/**
* Intercept the invocation of a {@link Test @Test} method.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @throws Throwable in case of failures
*/
default void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext extensionContext) throws Throwable {
invocation.proceed();
}

/**
* Intercept the invocation of a {@link TestFactory @TestFactory} method.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @param <T> the result type
* @return the result of the invocation; potentially {@code null}
* @throws Throwable in case of failures
*/
default <T> T interceptTestFactoryMethod(Invocation<T> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
return invocation.proceed();
}

/**
* Intercept the invocation of a {@link TestTemplate @TestTemplate} method.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @throws Throwable in case of failures
*/
default void interceptTestTemplateMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
invocation.proceed();
}

/**
* Intercept the invocation of a {@link DynamicTest}.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @throws Throwable in case of failures
*/
default void interceptDynamicTest(Invocation<Void> invocation, ExtensionContext extensionContext) throws Throwable {
invocation.proceed();
}

/**
* Intercept the invocation of an {@link AfterEach @AfterEach} method.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @throws Throwable in case of failures
*/
default void interceptAfterEachMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
invocation.proceed();
}

/**
* Intercept the invocation of an {@link AfterAll @AfterAll} method.
*
* @param invocation the invocation that is being intercepted; never
* {@code null}
* @param extensionContext the current extension context; never {@code null}
* @throws Throwable in case of failures
*/
default void interceptAfterAllMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
invocation.proceed();
}

/**
* An invocation that returns a result and may throw a {@link Throwable}.
*
* <p>This interface is not intended to be implemented by clients.
*
* @param <T> the result type
* @since 5.5
*/
@API(status = EXPERIMENTAL, since = "5.5")
interface Invocation<T> {

/**
* Proceed with this invocation.
*
* @return the result of this invocation; potentially {@code null}.
* @throws Throwable in case the invocation failed
*/
T proceed() throws Throwable;

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2015-2019 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import java.lang.reflect.Executable;
import java.util.List;
import java.util.Optional;

import org.apiguardian.api.API;

/**
* {@code ReflectiveInvocationContext} encapsulates the <em>context</em> of
* a reflective invocation of an executable (method or constructor).
*
* <p>This interface is not intended to be implemented by clients.
*
* @since 5.5
*/
@API(status = EXPERIMENTAL, since = "5.5")
public interface ReflectiveInvocationContext<T extends Executable> {

/**
* Get the target class of this invocation context.
*
* <p>If this invocation context represents an instance method, this
* method returns the class of the object the method will be invoked on,
* not the class it is declared in. Otherwise, i.e. if this invocation
* represents a static method or constructor, this method returns the
* class the method or constructor is declared in.
*
* @return the target class of this invocation context; never
* {@code null}
*/
Class<?> getTargetClass();

/**
* Get the method or constructor of this invocation context.
*
* @return the executable of this invocation context; never {@code null}
*/
T getExecutable();

/**
* Get the arguments of the executable in this invocation context.
*
* @return the arguments of the executable in this invocation context;
* never {@code null}
*/
List<Object> getArguments();

/**
* Get the target object of this invocation context, if available.
*
* <p>If this invocation context represents an instance method, this
* method returns the object the method will be invoked on. Otherwise,
* i.e. if this invocation context represents a static method or
* constructor, this method returns {@link Optional#empty() empty()}.
*
* @return the target of the executable of this invocation context; never
* {@code null} but potentially empty
*/
Optional<Object> getTarget();

}
Loading

0 comments on commit 8bb09ad

Please sign in to comment.