Skip to content

Commit

Permalink
Properly support JUnit 5 parameters on test methods
Browse files Browse the repository at this point in the history
This is done by deep-cloning the objects into the TCCL
before actually passing them to the test

Fixes #8251, fixes #8703, fixes #8978
  • Loading branch information
geoand committed May 17, 2020
1 parent d63095b commit c2f679e
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.it.arc;

import java.util.List;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.spi.InjectionPoint;
import javax.inject.Inject;
Expand All @@ -14,4 +16,51 @@ public InjectionPoint getInjectionPoint() {
return injectionPoint;
}

public DummyResult dummy(DummyInput dummyInput) {
return new DummyResult(dummyInput.getName() + "/"
+ dummyInput.getNestedDummyInput().getNums().stream().mapToInt(Integer::intValue).sum());
}

public static class DummyResult {
private final String result;

public DummyResult(String result) {
this.result = result;
}

public String getResult() {
return result;
}
}

public static class DummyInput {
private final String name;
private final NestedDummyInput nestedDummyInput;

public DummyInput(String name, NestedDummyInput nestedDummyInput) {
this.name = name;
this.nestedDummyInput = nestedDummyInput;
}

public String getName() {
return name;
}

public NestedDummyInput getNestedDummyInput() {
return nestedDummyInput;
}
}

public static class NestedDummyInput {
private final List<Integer> nums;

public NestedDummyInput(List<Integer> nums) {
this.nums = nums;
}

public List<Integer> getNums() {
return nums;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.it.main;

import static org.hamcrest.MatcherAssert.assertThat;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

import javax.inject.Inject;

import org.hamcrest.CoreMatchers;
import org.hamcrest.Matcher;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

import io.quarkus.it.arc.UnusedBean;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class MethodSourceTest {

@Inject
UnusedBean unusedBean;

@ParameterizedTest
@MethodSource("provideDummyInput")
public void testParameterResolver(UnusedBean.DummyInput dummyInput, Matcher<String> matcher) {
UnusedBean.DummyResult dummyResult = unusedBean.dummy(dummyInput);
assertThat(dummyResult.getResult(), matcher);
}

private static Collection<Arguments> provideDummyInput() {
return Arrays.asList(
Arguments.of(
new UnusedBean.DummyInput("whatever", new UnusedBean.NestedDummyInput(Arrays.asList(1, 2, 3))),
CoreMatchers.is("whatever/6")),
Arguments.of(
new UnusedBean.DummyInput("hi", new UnusedBean.NestedDummyInput(Collections.emptyList())),
CoreMatchers.is("hi/0")));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package io.quarkus.it.main;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Arrays;

import javax.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

import io.quarkus.it.arc.UnusedBean;
import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
@ExtendWith(ParameterResolverTest.UnusedBeanDummyInputResolver.class)
public class ParameterResolverTest {

@Inject
UnusedBean unusedBean;

@Test
public void testParameterResolver(UnusedBean.DummyInput dummyInput) {
UnusedBean.DummyResult dummyResult = unusedBean.dummy(dummyInput);
assertEquals("whatever/6", dummyResult.getResult());
}

public static class UnusedBeanDummyInputResolver implements ParameterResolver {

@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return UnusedBean.DummyInput.class.getName().equals(parameterContext.getParameter().getType().getName());
}

@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) throws ParameterResolutionException {
return new UnusedBean.DummyInput("whatever", new UnusedBean.NestedDummyInput(Arrays.asList(1, 2, 3)));
}
}

}
6 changes: 6 additions & 0 deletions test-framework/junit5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core</artifactId>
</dependency>
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<!-- Avoid adding this to the BOM / Version has to be kept in sync with what optaplanner uses otherwise the enforcer complains -->
<version>1.4.11.1</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
import io.quarkus.test.junit.callback.QuarkusTestAfterEachCallback;
import io.quarkus.test.junit.callback.QuarkusTestBeforeAllCallback;
import io.quarkus.test.junit.callback.QuarkusTestBeforeEachCallback;
import io.quarkus.test.junit.internal.DeepClone;
import io.quarkus.test.junit.internal.XStreamDeepClone;

//todo: share common core with QuarkusUnitTest
public class QuarkusTestExtension
Expand All @@ -87,6 +89,8 @@ public class QuarkusTestExtension
private static List<Object> beforeEachCallbacks = new ArrayList<>();
private static List<Object> afterEachCallbacks = new ArrayList<>();

private static DeepClone deepClone;

private ExtensionState doJavaStart(ExtensionContext context) throws Throwable {
Closeable testResourceManager = null;
try {
Expand Down Expand Up @@ -137,6 +141,7 @@ private ExtensionState doJavaStart(ExtensionContext context) throws Throwable {
AugmentAction augmentAction = curatedApplication.createAugmentor(TestBuildChainFunction.class.getName(), props);
StartupAction startupAction = augmentAction.createInitialRuntimeApplication();
Thread.currentThread().setContextClassLoader(startupAction.getClassLoader());
populateDeepCloneField(startupAction);

//must be done after the TCCL has been set
testResourceManager = (Closeable) startupAction.getClassLoader().loadClass(TestResourceManager.class.getName())
Expand Down Expand Up @@ -198,6 +203,11 @@ public void run() {
}
}

// keep it super simple for now, but we might need multiple strategies in the future
private void populateDeepCloneField(StartupAction startupAction) {
deepClone = new XStreamDeepClone(startupAction.getClassLoader());
}

private void populateCallbacks(ClassLoader classLoader) throws ClassNotFoundException {
ServiceLoader<?> quarkusTestBeforeAllLoader = ServiceLoader
.load(Class.forName(QuarkusTestBeforeAllCallback.class.getName(), false, classLoader), classLoader);
Expand Down Expand Up @@ -499,19 +509,12 @@ private Object runExtensionMethod(ReflectiveInvocationContext<Method> invocation
}
newMethod.setAccessible(true);

// the arguments were not loaded from TCCL so we need to try and "convert" if possible
// most of the time this won't be possible or necessary, but for the widely used enum case we need to do it
// this is a total hack, but...
// the arguments were not loaded from TCCL so we need to deep clone them into the TCCL
// because the test method runs from a class loaded from the TCCL
List<Object> originalArguments = invocationContext.getArguments();
List<Object> argumentsFromTccl = new ArrayList<>();
for (Object arg : originalArguments) {
if (arg != null && arg.getClass().isEnum()) {
argumentsFromTccl.add(Enum.valueOf((Class<Enum>) Class.forName(arg.getClass().getName(), false,
Thread.currentThread().getContextClassLoader()), arg.toString()));
} else {
// we can't do anything but hope for the best...
argumentsFromTccl.add(arg);
}
argumentsFromTccl.add(deepClone.clone(arg));
}

return newMethod.invoke(actualTestInstance, argumentsFromTccl.toArray(new Object[0]));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.quarkus.test.junit.internal;

/**
* Strategy to deep clone an object
*
* Used in order to clone an object loaded from one ClassLoader into another
*/
public interface DeepClone {

Object clone(Object objectToClone);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package io.quarkus.test.junit.internal;

import com.thoughtworks.xstream.XStream;

/**
* Super simple cloning strategy that just serializes to XML and deserializes it using xstream
*/
public class XStreamDeepClone implements DeepClone {

private final XStream xStream;

public XStreamDeepClone(ClassLoader classLoader) {
xStream = new XStream();
XStream.setupDefaultSecurity(xStream);
xStream.allowTypesByRegExp(new String[] { ".*" });
xStream.setClassLoader(classLoader);
}

public Object clone(Object objectToClone) {
final String serialized = xStream.toXML(objectToClone);
return xStream.fromXML(serialized);
}
}

0 comments on commit c2f679e

Please sign in to comment.