-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
JUnit 4 parameterized support as an extension (#715) #723
Conversation
f0cc940
to
97bbf3f
Compare
throws ParameterResolutionException { | ||
return parameters(extensionContext).map(ArrayList::new).map(l -> l.get(this.parameterCount.get())).filter( | ||
params -> params.length == parameterCount).orElseThrow( | ||
ParameterizedExtension::unMatchedAmountOfParametersException); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. :)
97bbf3f
to
c4a5c67
Compare
There was a problem hiding this 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);; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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()); | ||
} |
There was a problem hiding this comment.
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[] |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
b875b29
to
e8ade4b
Compare
public class ParameterizedExtension implements TestTemplateInvocationContextProvider, ParameterResolver { | ||
private static ExtensionContext.Namespace parameters = ExtensionContext.Namespace.create( | ||
ParameterizedExtension.class); | ||
private int parametersCollectionIndex = 0; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()
?
There was a problem hiding this 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; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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()
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
e8ade4b
to
90ba9e7
Compare
@marcphilipp The reason the parameter resolver is not provided through the additional extensions, is because:
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? |
@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 |
@marcphilipp No problem! I didn't exactly try to return the parameter resolver from 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. |
I'll look into it and get back to you. |
Although I was quite sure that this should already be supported, I've now added a test that verifies that in bae2768: A @LiamClark Can you try to change the code in this PR accordingly? |
ca6df6c
to
c465613
Compare
|
||
@API(Experimental) | ||
public class ParameterizedExtension implements TestTemplateInvocationContextProvider { | ||
private static ExtensionContext.Namespace parameters = ExtensionContext.Namespace.create( |
There was a problem hiding this comment.
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?
c465613
to
26d5fa7
Compare
Hi @LiamClark, Just wondering what the status of this effort is. Can you shed any light on the subject? |
I think this would be a perfect use case for container templates. |
Yep... could well be! |
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. 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! |
OK. Thanks for the feedback.
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.
26d5fa7
to
99d7f47
Compare
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
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. |
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
@API
annotations