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

Introduce TestInstanceFactory extension API #1443

Closed

Conversation

kdubb
Copy link
Contributor

@kdubb kdubb commented Jun 4, 2018

Overview

TestInstanceFactory is a new extension API that allows custom creation of test instance objects.

  • It must be applied at the class level.
  • If multiple TestInstanceFactory extensions are found to be registered on a test class an error is thrown.
  • Supports @Nested classes, ensuring that only one factory is registered on each level.

Issue: #672


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


Definition of Done

@kdubb
Copy link
Contributor Author

kdubb commented Jun 4, 2018

@sbrannen This extension solves issues for test instances that need to be vended in a special manner; specifically this greatly simplifies testing using CDI for Weld Junit. Would love to see this or version of this implemented very soon so I thought I'd take a stab at it. Let me know your thoughts and suggested changes, if any.

@codecov
Copy link

codecov bot commented Jun 4, 2018

Codecov Report

Merging #1443 into master will decrease coverage by 0.03%.
The diff coverage is 88.57%.

Impacted file tree graph

@@             Coverage Diff             @@
##             master   #1443      +/-   ##
===========================================
- Coverage     92.03%     92%   -0.04%     
- Complexity     3416    3428      +12     
===========================================
  Files           316     318       +2     
  Lines          8173    8204      +31     
  Branches        696     701       +5     
===========================================
+ Hits           7522    7548      +26     
- Misses          485     489       +4     
- Partials        166     167       +1
Impacted Files Coverage Δ Complexity Δ
...iter/api/extension/TestInstantiationException.java 0% <0%> (ø) 0 <0> (?)
...r/engine/descriptor/NestedClassTestDescriptor.java 100% <100%> (ø) 5 <0> (ø) ⬇️
...jupiter/engine/descriptor/ClassTestDescriptor.java 99.28% <100%> (+0.12%) 57 <10> (+9) ⬆️
...e/execution/DefaultTestInstanceFactoryContext.java 100% <100%> (ø) 3 <3> (?)
...jupiter/engine/execution/ExtensionValuesStore.java 87.14% <0%> (-1.43%) 21% <0%> (ø)

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 38e149f...c7241da. Read the comment docs.

Copy link
Member

@sormuras sormuras 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 PR, looks good so far and instantiation of nested classes is covered. Exception-handling needs some love, though.

* <p>Consult the documentation in {@link Extension} for details on
* constructor requirements.
*
* @since 5.0
Copy link
Member

Choose a reason for hiding this comment

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

5.3 :)

* @see #instantiateTestClass(Class, ExtensionContext)
* @see #instantiateNestedTestClass(Class, Object, ExtensionContext)
*/
@API(status = STABLE, since = "5.0")
Copy link
Member

Choose a reason for hiding this comment

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

Let's start with status = EXPERIMENTAL and since = "5.3".

* @param context the current extension context; never {@code null}
* @return The required test instance; never {@code null}
*/
Object instantiateTestClass(Class<?> testClass, ExtensionContext context) throws Exception;
Copy link
Member

Choose a reason for hiding this comment

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

Create an dedicated exception type that extends JUnitException. TestInstanceCreationException?

* @return The required test instance; never {@code null}
*/
Object instantiateNestedTestClass(Class<?> testClass, Object outerInstance, ExtensionContext context)
throws Exception;
Copy link
Member

Choose a reason for hiding this comment

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

Use same dedicated exception type as above.

"Too many TestInstanceFactory extensions [%s] registered on test class: %s", factoryNames,
testClass.getSimpleName());

throw new JUnitException(errorMessage);
Copy link
Member

Choose a reason for hiding this comment

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

At least throw an instance of ExtensionContextException.

if (factory == null) {
return invokeTestInstanceConstructor(outerInstance, registry, extensionContext);
}
else {
Copy link
Member

Choose a reason for hiding this comment

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

else not needed here.

return factory.instantiateTestClass(this.testClass, extensionContext);
}
}
catch (RuntimeException exception) {
Copy link
Member

Choose a reason for hiding this comment

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

Both catch are too generic. And please use BlacklistedExceptions.rethrowIfBlacklisted(cause);

/**
* Integration tests that verify support for {@link TestInstanceFactory}.
*
* @since 5.0
Copy link
Member

Choose a reason for hiding this comment

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

5.3

@marcphilipp marcphilipp added this to the 5.3 M1 milestone Jun 4, 2018
@kdubb
Copy link
Contributor Author

kdubb commented Jun 4, 2018

@sormuras Your changes have been pushed

@kdubb kdubb force-pushed the test-instance-factory-extensions branch from 1baecf1 to 583377c Compare June 4, 2018 21:28
@@ -241,6 +241,21 @@ Examples:
- `org.example.MyCondition`: deactivates the condition whose FQCN is exactly
`org.example.MyCondition`.

[[extensions-test-instance-factories]]
=== Test Instance Factories
`{TestInstanceFactory}` defines the API for `Extensions` that wish to _provide new_
Copy link
Member

Choose a reason for hiding this comment

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

A corresponding link must also be added to /documentation/src/docs/asciidoc/link-attributes.adoc.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

[WARNING]
====
Registering multiple extensions that implement `{TestInstanceFactory}` will result in an
exception being thrown for all tests in the test class. It is the users responsibility to
Copy link
Member

Choose a reason for hiding this comment

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

... or test class hierarchy

Copy link
Member

Choose a reason for hiding this comment

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

users --> user's

* @since 5.3
*/
@API(status = EXPERIMENTAL, since = "5.3")
public class TestInstanceCreationException extends JUnitException {
Copy link
Member

Choose a reason for hiding this comment

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

I think TestInstantiationException would be a more suitable name

*
* <p>Extensions that implement {@code TestInstanceFactory} must be
* registered at the class level.
*
Copy link
Member

Choose a reason for hiding this comment

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

The class-level JavaDoc for TestInstanceFactory should also mention that only one such extension may be registered per test class or test class hierarchy, analogous to the documentation in the User Guide.

* only attempt to create the required test instance.
*
* @param testClass the test class to instantiate or otherwise obtain
* @param outerInstance instance of outer test class (if any)
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 method is solely dedicated to instantiating a nested test class (i.e., a non-static nested class), it then does not make sense for the outerInstance to ever be null. Thus the documentation must state ;never {@code null}.

* @return The required test instance; never {@code null}
* @throws TestInstanceCreationException when an error occurs with the invocation of a factory
*/
Object instantiateNestedTestClass(Class<?> testClass, Object outerInstance, ExtensionContext context)
Copy link
Member

Choose a reason for hiding this comment

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

Another option would be to have a single such method in this new API and make the outerInstance an Optional.


if (parentRegistry != null) {
List<TestInstanceFactory> parentFactories = parentRegistry.getExtensions(TestInstanceFactory.class);
factories.removeAll(parentFactories);
Copy link
Member

Choose a reason for hiding this comment

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

If we are going to ignore factories registered in parent contexts -- if and only if there is a locally registered factory -- we then need to explicitly document that behavior.

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 have clarified the behavior in both the javadoc and user guide.

Copy link
Member

Choose a reason for hiding this comment

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

ok. thanks

}
catch (Throwable exception) {
BlacklistedExceptions.rethrowIfBlacklisted(exception);
throw exception;
Copy link
Member

Choose a reason for hiding this comment

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

This combination does not make sense.

What are you trying to achieve?

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 misunderstood what @sormuras had suggested and exactly wash rethrowIfBlacklisted was to be used for. I have just removed this handling entirely to let the exception propagate as normal.


try {
if (outerInstance != null) {
return factory.instantiateNestedTestClass(this.testClass, outerInstance, extensionContext);
Copy link
Member

Choose a reason for hiding this comment

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

This approach makes it impossible for such a constructor to accept arguments provided by a ParameterResolver -- for example, TestInfo, TestReporter, mocks from Mockito, beans from Spring, etc.

We need to decide if we want this new extension API to suffer from this restriction.

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 do not believe this is a restriction that impacts the majority of use cases for factories. I believe factories are specifically for when customized construction is required by external libraries; in which case arguments from JUnit 5 sources are probably not going to be required. Also, the current extension API seems to cover these situations well and using a factory doesn't negate post-processing or any other extension.

@sbrannen sbrannen changed the title Add TestInstanceFactory extension point Introduce TestInstanceFactory extension API Jun 5, 2018
@kdubb
Copy link
Contributor Author

kdubb commented Jun 5, 2018

@sbrannen I believe the changes you've requested have been pushed. Let me know if anything is still outstanding.

@@ -65,6 +65,7 @@ endif::[]
:RepetitionInfo: {javadoc-root}/org/junit/jupiter/api/RepetitionInfo.html[RepetitionInfo]
:TestExecutionExceptionHandler: {javadoc-root}/org/junit/jupiter/api/extension/TestExecutionExceptionHandler.html[TestExecutionExceptionHandler]
:TestInfo: {javadoc-root}/org/junit/jupiter/api/TestInfo.html[TestInfo]
:TestInstanceFactory: {javadoc-root}/org/junit/jupiter/api/extension/TestInstanceFactory.html[TestInstanceFactoryh]
Copy link
Member

Choose a reason for hiding this comment

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

Remove trailing "h" in link name.

TestInstanceFactoryh --> TestInstanceFactory

@sbrannen
Copy link
Member

sbrannen commented Jun 6, 2018

My comments:

This approach makes it impossible for such a constructor to accept arguments provided by a ParameterResolver -- for example, TestInfo, TestReporter, mocks from Mockito, beans from Spring, etc.

We need to decide if we want this new extension API to suffer from this restriction.

Response from @kdubb:

I do not believe this is a restriction that impacts the majority of use cases for factories. I believe factories are specifically for when customized construction is required by external libraries; in which case arguments from JUnit 5 sources are probably not going to be required. Also, the current extension API seems to cover these situations well and using a factory doesn't negate post-processing or any other extension.

@junit-team/junit-lambda, the team needs to reach a consensus on this before continuing with this PR.

@sbrannen
Copy link
Member

sbrannen commented Jun 6, 2018

@kdubb,

I believe the changes you've requested have been pushed.

Thanks

Let me know if anything is still outstanding.

I noticed an extra "h", but I think that's all for now.

* @return the required test instance; never {@code null}
* @throws TestInstantiationException when an error occurs with the invocation of a factory
*/
Object instantiateTestClass(Class<?> testClass, Optional<Object> outerInstance, ExtensionContext context)
Copy link
Member

Choose a reason for hiding this comment

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

Since the team already foresees that we might eventually need to add a callback into the existing parameter resolution mechanism, I think it would be better to use a parameter object, e.g. TestInstanceFactoryContext that groups testClass and outerInstance. This would allow us to later add a method to the context etc. (see ParameterResolver/ParameterContext for an example of this approach).

Copy link
Member

Choose a reason for hiding this comment

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

👍

@sormuras sormuras modified the milestones: 5.3 M1, 5.3 M2 Jun 15, 2018
@kdubb
Copy link
Contributor Author

kdubb commented Jun 19, 2018

@marcphilipp @sbrannen Not sure if you guys saw but I committed the changes @marcphilipp requested. Is there anything else that needs to happen? Would have love to have this in M1 if possible.

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 think this PR is almost ready to be merged. I'm missing a test for @TestInstance(PER_CLASS). Could you please add that?

@marcphilipp marcphilipp removed their assignment Jun 24, 2018
@kdubb kdubb force-pushed the test-instance-factory-extensions branch from 837d29a to 52b168a Compare June 29, 2018 01:37
kdubb added 3 commits June 28, 2018 18:50
`TestInstanceFactory` is a new extension point that allows custom creation of test instance objects.
* It must be applied at the class level
* If multiple `TestInstanceFactories` are found to be registered on a test class an error is thrown
* Supports @nested classes, ensuring that only one factory is registered on each level
* Declare `TestInstanceFactory` methods to throw `TestInstanceCreationException`
* Mark new API as `EXPERIMENTAL`
* Mark relevant files as `@since 5.3`
kdubb and others added 5 commits June 28, 2018 18:51
* Combine API methods into one; no compelling reason for separating top-level and nested factory methods.
* Rename TestInstanceCreationException -> TestInstantiationException
* Clarify documenation & javadoc on nested factory handling
* Remove incorrect exception “filtering”
* Added `TestInstanceFactoryContext` to encapsulate the information a `TestInstanceFactory` _may_ need to instantiate a test class
* Changed signature of `instantiateTestClass` to use `TestInstanceFactoryContext` instead of individual parameters
@kdubb kdubb force-pushed the test-instance-factory-extensions branch from 52b168a to 21e2788 Compare June 29, 2018 01:52
@kdubb
Copy link
Contributor Author

kdubb commented Jun 29, 2018

@marcphilipp Test for PER_CLASS was added and rebased onto latest master

@kdubb kdubb force-pushed the test-instance-factory-extensions branch from 21e2788 to c7241da Compare June 29, 2018 02:02
@sbrannen
Copy link
Member

Thanks for adding the additional tests, @kdubb.

@sbrannen
Copy link
Member

This has been merged into master in 5b4c642 and polished in subsequent commits.

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.

4 participants