Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JUnit 4 parameterized support as an extension (#715) #723

Conversation

LiamClark
Copy link
Contributor

@LiamClark LiamClark commented Mar 11, 2017

Overview

This allows running parameterized tests from JUnit 4 with Field and Constructor injection, fixes #715.
Fail silently when asked whether we can support the test case,
but fail explicitly with an exception when parameters can't be resolved.

It does this by combining a TestTemplateContextProvider that adds one context for every Object[] in the parameters Collection. These contexts contain an additional extension that performs the field injection if required.

However, when constructor injection is used, there needs to be a parameter resolver in place before the TestTemplateContextProvider is even triggered, therefore the parameter resolver is not an additional extension. Having the parameter resolver on the top level causes us to lose the context of which TemplateContext we were in and this information has to be regained by carefully counting the parameter resolutions. Is there a cleaner way to solve this?

I Store the results of the parameters factory function In the ClassContext store,
so we only call the parameters function once.

The tests are all integration tests, since doing unit tests resulted in unwieldy mocking, however, the amount of 'TestSubjectClasses' needed seems a bit much any idea's to clear this up?

This also ties in with the production code, I tried to keep everything as private as possible, maybe the tests could be simpler if I open up the access modifiers a bit, what would your design goals be here?

I tried to follow the behavior of JUnit 4 as closely as possible are there perhaps any behaviors missing?

Lastly, there is a lot of passing around of ExtensionContexts perhaps extracting a class to hold this could simplify the code a bit.

I hereby agree to the terms of the JUnit Contributor License Agreement.


Definition of Done

@LiamClark LiamClark force-pushed the parameterized-migration-support branch from f0cc940 to 97bbf3f Compare March 11, 2017 13:10
throws ParameterResolutionException {
return parameters(extensionContext).map(ArrayList::new).map(l -> l.get(this.parameterCount.get())).filter(
params -> params.length == parameterCount).orElseThrow(
ParameterizedExtension::unMatchedAmountOfParametersException);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've had to put quite some effort to understand the code in this class because of the extensive use of lambdas. It is probably me... I am not smart enough or young enough to understand these(... well definitely both) :)

I was especially troubled by the above lambda chain. I think it would have helped me if this.parameterCount was named differently. In this case it has the same name as the int parameterCount. I think a more descriptive name would be something like parametersCollectionSize or parameterArraysCount

Copy link
Contributor Author

@LiamClark LiamClark Mar 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gaganis ah yes definitely, that would be a much clearer name.
I was also a bit unhappy about this one. Let's see what it looks like with the new name, maybe changing some of the lambdas to functions with names might help too

public class ParameterizedExtension implements TestTemplateInvocationContextProvider, ParameterResolver {
private static ExtensionContext.Namespace parameters = ExtensionContext.Namespace.create(
ParameterizedExtension.class);;
private AtomicInteger parameterCount = new AtomicInteger();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you use an AtomicInteger here? Do reads and writes need to be atomic for this?

Thanks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mainly because I had this piece of code in the back of my head :
https://github.com/junit-team/junit5/blob/master/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java#L93

but it seems that the AtomicInteger there is used to get around the effectively final rule rather than atomicity.
It seems that resolve is called sequentially so it could just be a normal int.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I rewrote TestTemplateTestDescriptor in #725 to remove the AtomicInteger. :)

@LiamClark LiamClark force-pushed the parameterized-migration-support branch from 97bbf3f to c4a5c67 Compare March 11, 2017 14:41
Copy link
Contributor

@jbduncan jbduncan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a handful of minor suggestions to help make this code readable. :)

@API(Experimental)
public class ParameterizedExtension implements TestTemplateInvocationContextProvider, ParameterResolver {
private static ExtensionContext.Namespace parameters = ExtensionContext.Namespace.create(
ParameterizedExtension.class);;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two semi-colons? :)


@Override
public Iterator<TestTemplateInvocationContext> provide(ContainerExtensionContext context) {
//grabbing the parent ensures the paremeters are stored in the same store.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space between comment start and "grabbing"?

Object[] parameters = resolveParametersForConstructor(extensionContext, parameterCount);

int parameterIndex = parameterContext.getIndex();
//move to the next set of parametersFields
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space between comment start and "move"?

throws ParameterResolutionException {
return parameters(extensionContext).map(ArrayList::new).map(l -> l.get(this.parametersCollectionIndex)).filter(
params -> params.length == parameterCount).orElseThrow(
ParameterizedExtension::unMatchedAmountOfParametersException);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using // @formatter:{off|on} to make this more readable:

// @formatter:off
return parameters(extensionContext).map(ArrayList::new)
    .map(l -> l.get(this.parametersCollectionIndex))
    .filter(params -> params.length == parameterCount)
    .orElseThrow(ParameterizedExtension::unMatchedAmountOfParametersException);
// @formatter:on


private static List<Integer> parameterIndexes(List<Field> fields) {
return fields.stream().map(f -> f.getAnnotation(Parameterized.Parameter.class)).map(
Parameterized.Parameter::value).collect(toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using // @formatter:{off|on} here and all the other stream-using methods below.

it -> it.getPayload(TestExecutionResult.class)).map(
o -> o.flatMap(TestExecutionResult::getThrowable)).filter(Optional::isPresent).map(
Optional::get).collect(Collectors.toList());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another case where // @formatter:{off|on} would help readability.

*
* @return the object[] for this parameter iteration.
* @throws ParameterResolutionException If the amount of arguments of the constructor doesn't match the amount
* of arguments of the currently resolved object[]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

object[] instead of Object[]?

}

@Override
public Iterator<TestTemplateInvocationContext> provide(ContainerExtensionContext context) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry, but I've changed provide to return a Stream in 410b47d which has just been merged into master. Please rebase this pull request and update this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcphilipp no problem at all, the iterator was being created from a stream anyways. So it actually simplified the code!

public class ParameterizedExtension implements TestTemplateInvocationContextProvider, ParameterResolver {
private static ExtensionContext.Namespace parameters = ExtensionContext.Namespace.create(
ParameterizedExtension.class);
private int parametersCollectionIndex = 0;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this index should actually be saved in the store, since the extension is required to be stateless, however before changing that. I'd like to know if there is any way that might avoid this index.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, instead of implementing ParameterResolver in the extension itself, can you return a ParameterResolver from TestTemplateInvocationContext.getAdditionalExtensions()?

Copy link
Member

@marcphilipp marcphilipp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the general direction, though! 👍

public class ParameterizedExtension implements TestTemplateInvocationContextProvider, ParameterResolver {
private static ExtensionContext.Namespace parameters = ExtensionContext.Namespace.create(
ParameterizedExtension.class);
private int parametersCollectionIndex = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, instead of implementing ParameterResolver in the extension itself, can you return a ParameterResolver from TestTemplateInvocationContext.getAdditionalExtensions()?

};
}

private static class InjectionExtension implements BeforeTestExecutionCallback {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a TestInstancePostProcessor instead?

* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.migrationsupport.rules;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should reside in a different package. How about org.junit.jupiter.migrationsupport.parameterized?


private static Optional<Collection<Object[]>> parameters(ExtensionContext context) {
return context.getStore(parameters).getOrComputeIfAbsent("parameterMethod",
k -> new ParameterWrapper(callParameters(context)), ParameterWrapper.class).getValue();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be no need to store the parameters if we make all extensions "local" and handle them in provide().

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcphilipp It is moved into provide now :), but if there are multiple test templates in the class, it would still trigger the parameters method twice.

However is calling the parameters method once something we care about?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You sure? You already store it in the parent context's Store but only if it's not already there, don't you? Can you add a test for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcphilipp Yes, I already did here https://github.com/junit-team/junit5/pull/723/files#diff-c8fe8d61d6bba040fc43c22d9db3f16aR291 (Looking at it now it could definitely have a better name)

So in the current implementation the parameters method is indeed only called once, but it does still require storing them in the store. My comment was intended to show that even though the extensions are now local, I still need to store them for this behaviour.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, ok, I think that's acceptable in order to preserve the old behavior. 👍

return (Collection<Object[]>) o;
}
else {
throw wrongParametersReturnType();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameters methods may also return an Iterable or an Object[] (cf. https://github.com/junit-team/junit4/blob/master/src/main/java/org/junit/runners/Parameterized.java#L288-L297). You may want to use CollectionUtils.toStream(o) even though it supports additional types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marcphilipp is this only for the single parameter case or also in general?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@LiamClark LiamClark force-pushed the parameterized-migration-support branch from e8ade4b to 90ba9e7 Compare April 9, 2017 15:11
@LiamClark
Copy link
Contributor Author

@marcphilipp The reason the parameter resolver is not provided through the additional extensions, is because:

However, when constructor injection is used, there needs to be a parameter resolver in place before the TestTemplateContextProvider is even triggered, therefore the parameter resolver is not an additional extension. Having the parameter resolver on the top level causes us to lose the context of which TemplateContext we were in and this information has to be regained by carefully counting the parameter resolutions. Is there a cleaner way to solve this?

If no parameter resolver is present for the first constructor the test fails before the test template can even be considered. Perhaps we can resolve only the first constructor invocation from the top level parameter resolver and supply the rest of the parameters through the additional extensions?

@marcphilipp
Copy link
Member

@LiamClark Sorry for not re-reading your comment properly. I now remember reading your comment when you opened the pull request. 😳

It sounds like you've actually tried returning the ParameterResolver from getAdditionalExtensions(), right?

@LiamClark
Copy link
Contributor Author

@marcphilipp No problem!

I didn't exactly try to return the parameter resolver from getAdditionalExtensions(). It was indeed my original idea, but this problem showed it self simply trying to get the extension callback to trigger.

It of course would be the nicest spot to resolve the parameters in, but it seems like the mismatch between Junit4 and Jupiter is slightly to big.

@marcphilipp
Copy link
Member

I'll look into it and get back to you.

@marcphilipp
Copy link
Member

Although I was quite sure that this should already be supported, I've now added a test that verifies that in bae2768: A ParameterResolver returned by a TestTemplateInvocationContext may be used to resolve constructor arguments.

@LiamClark Can you try to change the code in this PR accordingly?

@LiamClark LiamClark force-pushed the parameterized-migration-support branch 2 times, most recently from ca6df6c to c465613 Compare April 10, 2017 15:13

@API(Experimental)
public class ParameterizedExtension implements TestTemplateInvocationContextProvider {
private static ExtensionContext.Namespace parameters = ExtensionContext.Namespace.create(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also be final and then follow the naming convention for constants, right?

@sbrannen
Copy link
Member

sbrannen commented Jan 9, 2018

Hi @LiamClark,

Just wondering what the status of this effort is.

Can you shed any light on the subject?

@marcphilipp
Copy link
Member

I think this would be a perfect use case for container templates.

@sbrannen
Copy link
Member

sbrannen commented Jan 9, 2018

I think this would be a perfect use case for container templates.

Yep... could well be!

@LiamClark
Copy link
Contributor Author

So I believe all feedback in the last set of requested changes was actually implemented.

So mainly it would be adding it to the user guide, if you guys still want it.
It is however based on a now somewhat dated version of master.

I'd be willing to do the rebase and fix the code or if you want to explore the possibilities to use container templates for this, I have some time the next few weeks to do that as well.

let me know!

@sbrannen
Copy link
Member

So I believe all feedback in the last set of requested changes was actually implemented.

OK. Thanks for the feedback.

I'd be willing to do the rebase and fix the code or if you want to explore the possibilities to use container templates for this, I have some time the next few weeks to do that as well.

Well, container templates don't exist yet. So let's hold off on rebasing/reworking until we make progress on container templates.

This allows for Field and Constructor injection.
Fail silently when asked whether we can support the test case,
but fail explicitly with an exception when parameters can't be resolved.

Store the results of the parameters factory function
In the ClassContext store
This also includes support for single parameter tests.
@LiamClark LiamClark force-pushed the parameterized-migration-support branch from 26d5fa7 to 99d7f47 Compare February 13, 2018 20:37
@codecov
Copy link

codecov bot commented Feb 13, 2018

Codecov Report

Merging #723 into master will decrease coverage by 0.02%.
The diff coverage is n/a.

Impacted file tree graph

@@             Coverage Diff              @@
##             master     #723      +/-   ##
============================================
- Coverage     92.01%   91.99%   -0.03%     
+ Complexity     3252     3159      -93     
============================================
  Files           299      296       -3     
  Lines          7779     7666     -113     
  Branches        670      626      -44     
============================================
- Hits           7158     7052     -106     
+ Misses          466      447      -19     
- Partials        155      167      +12
Impacted Files Coverage Δ Complexity Δ
...atform/engine/support/descriptor/FilePosition.java 73.07% <0%> (-22.58%) 13% <0%> (-11%)
...ne/support/descriptor/ClasspathResourceSource.java 75% <0%> (-20.84%) 12% <0%> (-2%)
...c/main/java/org/junit/jupiter/api/DynamicNode.java 80% <0%> (-20%) 2% <0%> (-2%)
...platform/engine/support/descriptor/FileSource.java 73.07% <0%> (-15.39%) 13% <0%> (-1%)
...it/platform/commons/support/AnnotationSupport.java 85.71% <0%> (-14.29%) 5% <0%> (-2%)
...engine/support/descriptor/CompositeTestSource.java 86.66% <0%> (-13.34%) 7% <0%> (-2%)
...tform/engine/support/descriptor/PackageSource.java 87.5% <0%> (-12.5%) 9% <0%> (-2%)
...ter/params/ParameterizedTestParameterResolver.java 87.5% <0%> (-9.87%) 7% <0%> (-9%)
...ter/params/converter/DefaultArgumentConverter.java 78.66% <0%> (-8%) 8% <0%> (-2%)
...unit/platform/engine/discovery/MethodSelector.java 78% <0%> (-8%) 15% <0%> (-2%)
... and 109 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 948841c...99d7f47. Read the comment docs.

@marcphilipp
Copy link
Member

We're cleaning out the issue tracker and closing PRs for issues that we've not seen much demand to fix. Feel free to comment with additional justifications if you feel that this one should not have been closed.

@marcphilipp marcphilipp removed this from the General Backlog milestone May 4, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Introduce migration support for JUnit 4's Parameterized Runner
5 participants