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

@BeforeParam/@AfterParam for Parameterized runner #1435

Merged
merged 18 commits into from
Apr 21, 2017

Conversation

panchenko
Copy link
Contributor

This PR addresses issue #45.
Some @ParamRule could also be introduced.

Copy link
Member

@kcooney kcooney left a comment

Choose a reason for hiding this comment

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

Thanks for the contribution!

Sorry for the delay. I was hoping one of the other maintainers would get to this, since I'm not as familiar with Parameterized.

I pushed some minor style fixes to your branch. I think only thing missing is a bit of test coverage.

}

@Test
public void beforeParamAndAfterParamAreRun() {
Copy link
Member

Choose a reason for hiding this comment

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

It would be great to have tests for

  1. Multiple @BeforeParam methods
  2. Multiple @AfterParam methods
  3. @AfterParam methods with parameters
  4. @BeforeParam methods without parameters
  5. Parameterized tests with more than one parameter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kcooney I've added some more tests and validation for number of parameters of the BeforeParam/AfterParam methods.
Do you think it's OK already?

@panchenko
Copy link
Contributor Author

@kcooney Sounds good, I will add more tests.
Could you please clarify one thing on the style changes: the project uses google code style, the line length is 100 there, seems like lines were smaller already, however you still wrapped some, to make them even shorter.

@kcooney
Copy link
Member

kcooney commented Apr 18, 2017

@panchenko yes we allow lines up to 100 characters. Sorry about that, I was reviewing the GitHub diff view when I did the cleanup. The only minor issue is that we do not use final for fields or parameters unless forced to by the compiler (so final on a field is a hint it is used in an anonymous class)


private RunnersFactory(Class<?> klass) {
testClass = new TestClass(klass);
}

private void evaluateParameters() throws Throwable {
Copy link
Member

Choose a reason for hiding this comment

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

This isn't thread-safe, which can be a problem if tests are run in parallel. Also, having this method have a side-effect of assigning allParameters is a bit odd.

If we really need to cache the result of calling allParameters() I would prefer that to happen inside of allParameters() itself. You could rename allParameters() to callParametersMethod() and then add this:

private volatile Iterable<Object> allParameters;

private Iterable<Object> allParameters() throws Throwable {
  if (allParameters == null) { // volatile read
    allParameters = callParametersMethod();
  }
  return allParameters;
}

(and then change all the places where you reference allParameters to call allParmeters() like was done before)

This won't guarantee that the the parameters method is only called once, but the only way to guarantee that would be
to have a lock that is held while we call the parameters 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.

Thread safety is not an issue here, as an instance of this inner class exists only during the Parameterized constructor invocation.

private void validateBeforeParamAndAfterParamMethods(Integer parameterCount)
throws InvalidTestClassError {
List<Throwable> errors = new ArrayList<Throwable>();
validatePublicStaticVoidMethods(Parameterized.BeforeParam.class, parameterCount, errors);
Copy link
Member

Choose a reason for hiding this comment

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

Could we delete validateBeforeParamAndAfterParamMethods() and instead override collectInitializationErrors(List<Throwable>) and call validatePublicStaticVoidMethods there?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried that before, collectInitializationErrors() is called from the super class constructor and the number of method parameters can't be easily validated (only with some tricks, e.g. using ThreadLocal).

Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. I've never been a fan of calling non-static methods in constructors. It's caused no end of problems in JUnit4. Hopefully they avoided that in the JUnit5 code base

getParametersRunnerFactory()));
}

private Integer getParameterCount() throws Throwable {
evaluateParameters();
Iterator<Object> iterator = allParameters.iterator();
Copy link
Member

Choose a reason for hiding this comment

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

I don't think there's any guarantee that the Iterable can be iterated more than once. The current code only iterates over the collection once, so this could possibly break people.

I think we might need to wait until we are about to call a BeforeParam or AfterParam method before we check if the method has the right number of parameters.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point on iterating it multiple times. I would like to validate these methods once, otherwise an error would in each child runner (they are created for each parameter).

private TestWithParameters createTestWithNotNormalizedParameters(
String pattern, int index, Object parametersOrSingleParameter) {
Object[] parameters = (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter
: new Object[] { parametersOrSingleParameter };
if (parameterCount == null) {
parameterCount = parameters.length;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a side effect we could have a check that number of parameters is always the same.

Copy link
Member

Choose a reason for hiding this comment

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

If the number of parameters were different for some parameter sets, you would get a failure when the child runner is run.

Copy link
Member

@kcooney kcooney left a comment

Choose a reason for hiding this comment

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

Thanks for adding more tests!

private void validateBeforeParamAndAfterParamMethods(Integer parameterCount)
throws InvalidTestClassError {
List<Throwable> errors = new ArrayList<Throwable>();
validatePublicStaticVoidMethods(Parameterized.BeforeParam.class, parameterCount, errors);
Copy link
Member

Choose a reason for hiding this comment

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

Ah, I see. I've never been a fan of calling non-static methods in constructors. It's caused no end of problems in JUnit4. Hopefully they avoided that in the JUnit5 code base

private TestWithParameters createTestWithNotNormalizedParameters(
String pattern, int index, Object parametersOrSingleParameter) {
Object[] parameters = (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter
: new Object[] { parametersOrSingleParameter };
if (parameterCount == null) {
parameterCount = parameters.length;
}
Copy link
Member

Choose a reason for hiding this comment

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

If the number of parameters were different for some parameter sets, you would get a failure when the child runner is run.

@@ -276,10 +330,15 @@ private ParametersRunnerFactory getParametersRunnerFactory()
}
}

private Integer parameterCount;
Copy link
Member

Choose a reason for hiding this comment

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

Having this set as a side-effect of calling createRunners() is fragile. It would be better to make this final and assign this at construction time. I added a commit that does ths.

if (parameters instanceof List) {
return (List<Object>) parameters;
} else if (parameters instanceof Collection) {
return new ArrayList<Object>((Collection<Object>) parameters);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kcooney IMHO there is no need to handle Collection specially, it can be treated as Iterable.

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.

Overall, I think it looks great! I would like to get rid of the duplication, though.

In addition, I think we should add an example to the Javadoc of Parameterized.

}
next.evaluate();
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Can we reuse org.junit.internal.runners.statements.RunBefores and pass parameters to it?

}
}
MultipleFailureException.assertEmpty(errors);
}
Copy link
Member

Choose a reason for hiding this comment

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

Can we reuse org.junit.internal.runners.statements.RunAfters and pass parameters to it?

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.

Thanks for making the changes this fast! We're almost there, I have a few additional nitpicks.

* <p>
* If your test needs to perform some preparation or cleanup based on the parameters this can be
* done by adding public static methods annotated with &#064;BeforeParam/&#064;AfterParam,
* such methods should either have no parameters or have same parameters as the test.
Copy link
Member

Choose a reason for hiding this comment

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

Please replace this paragraph with:

 * If your test needs to perform some preparation or cleanup based on the
 * parameters, this can be done by adding public static methods annotated with
 * {@code @BeforeParam}/{@code @AfterParam}. Such methods should either have no
 * parameters or the same parameters as the test.

* System.out.println("Testing " + onlyParameter);
* }
* </pre>
* </p>
Copy link
Member

Choose a reason for hiding this comment

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

Please remove the </p> line.

} catch (Throwable e) {
errors.add(e);
}
}
}
MultipleFailureException.assertEmpty(errors);
}

protected void invokeMethod(FrameworkMethod method) throws Throwable {
Copy link
Member

Choose a reason for hiding this comment

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

Please add @since 4.13

}
next.evaluate();
}

protected void invokeMethod(FrameworkMethod method) throws Throwable {
Copy link
Member

Choose a reason for hiding this comment

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

Please add @since 4.13

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.

LGTM! 👍

@kcooney kcooney merged commit 1bf8438 into junit-team:master Apr 21, 2017
@kcooney
Copy link
Member

kcooney commented Apr 21, 2017

Thanks, @panchenko ! Could you please update the release notes at https://github.com/junit-team/junit4/wiki/4.13-Release-Notes ?

@panchenko panchenko deleted the issue45 branch April 21, 2017 07:39
@panchenko
Copy link
Contributor Author

Updated it, thanks @kcooney!

@raffig
Copy link

raffig commented Aug 22, 2019

I have one doubt: Currently @BeforeParam is called BEFORE parameters are known (before test instance constructor) thus annotated metod cannot do nothing that depends on parameter value. Of course one can do this in constructor, but I'm not sure if that is by the book. @AfterParam seems to work as expected.

@raffig
Copy link

raffig commented Aug 23, 2019

And the second doubt: @AfterParam method must be static. If this method is used to cleanup some resources passed as parameters (e.g. test container) it causes the resources must be kept in static field. Won't this break anything like e.g. parallel test execution?

Could you please take a closer look at this:
testcontainers/testcontainers-java#1774

@panchenko
Copy link
Contributor Author

@raffig both must be public static and optionally can take parameters (however that's not clear from the documentation).

@raffig
Copy link

raffig commented Aug 23, 2019

@panchenko Thanks! Thats a great hint with parameters. I will fix my workaround and paste it in testcontainers thread. However I think maybe @ClassRule/TestRule could be fixed as well, so TestContainer could implement properly the container lifecycle in parametrized test?

@panchenko
Copy link
Contributor Author

@raffig @ClassRule works as expected - for the whole class

@raffig
Copy link

raffig commented Aug 23, 2019

@panchenko Ah, ok, so if ClassRule is for whole class, so ParamRule is needed right? Do you think it will get introduced in 4.13?

Thanks to your advice I think I've created suitable solution for TestContainers: testcontainers/testcontainers-java#1774 (comment)

Probably the same could be implemented with something like ParamRule.

@panchenko
Copy link
Contributor Author

@raffig It feels like quite a rare case when a TestRule instance is used as a test parameter.

@raffig
Copy link

raffig commented Aug 23, 2019

@panchenko I didn't think about TestRule as parameter. Rather about something that would allow parameters lifecycle management more out-of-the box as in case of TestContainers. However the longer I'm thinking about this the closer I get to the conclusion that is not possible to do in any useful generic way. As for now I think the @BeforeParam/@AfterParam is the best option to solve this.

Everything started when I found out that ClassRule provided by TestContainers does not work in case of Parameterized tests. That's why I've been searching for solution in the beginning, which I found using new @BeforeParam/@AfterParam. Now I'm wondering if TestContainers could provide something like ClassRule, out-of-the-box, but working with Parameterized tests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants