junit5
junit5 copied to clipboard
Extension registration does not work for non static fields
Steps to reproduce
I am trying to do the same as shown in documentation in https://junit.org/junit5/docs/current/user-guide/#extensions-registration-programmatic, where the following example appears:
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
class RandomNumberDemo {
// use random number field in test methods and @BeforeEach
// or @AfterEach lifecycle methods
@Random
private int randomNumber1;
/* . . . */
}
However, my extension (only implements BeforeAllCallback) only gets invoked when private int randomNumber1; is made static private static int randomNumber1;.
I don't know if this is an issue with the code or the documentation. Any clarifications would be appreciated.
Context
- Used versions (Jupiter/Vintage/Platform): Jupiter 5.9.0
- Build Tool/IDE: Maven & IntelliJ
Deliverables
- [ ] ...
However, my extension (only implements
BeforeAllCallback) only gets invoked whenprivate int randomNumber1;is made staticprivate static int randomNumber1;. I don't know if this is an issue with the code or the documentation. Any clarifications would be appreciated.
I'd say that's by design and to be expected.
See org.junit.jupiter.engine.descriptor.ExtensionUtils.registerExtensionsFromFields(ExtensionRegistrar, Class<?>, Object) for details.
However, we could revisit this behavior if you really think it's in need of change (and if it's feasible to change the behavior).
For what it's worth, here's the actual implementation of that extension which I never "published" (or at least I don't recall having published it) since it's merely a proof of concept.
/*
* Copyright 2015-2022 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/
package example.extensions;
import static org.junit.platform.commons.support.AnnotationSupport.findAnnotatedFields;
import java.lang.reflect.Field;
import java.util.function.Predicate;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.HierarchyTraversalMode;
import org.junit.platform.commons.support.ModifierSupport;
class RandomNumberExtension implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {
private final java.util.Random random = new java.util.Random();
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.isAnnotated(Random.class) && isInteger(parameterContext.getParameter().getType());
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return this.random.nextInt();
}
@Override
public void beforeAll(ExtensionContext context) {
injectFields(context.getRequiredTestClass(), null, ModifierSupport::isStatic);
}
@Override
public void beforeEach(ExtensionContext context) {
injectFields(context.getRequiredTestClass(), context.getRequiredTestInstance(), ModifierSupport::isNotStatic);
}
private boolean isInteger(Class<?> type) {
return type == Integer.class || type == int.class;
}
private void injectFields(Class<?> testClass, Object instance, Predicate<Field> predicate) {
predicate = predicate.and(field -> isInteger(field.getType()));
findAnnotatedFields(testClass, Random.class, predicate, HierarchyTraversalMode.TOP_DOWN).forEach(field -> {
field.setAccessible(true);
try {
field.set(instance, this.random.nextInt());
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
});
}
}
However, we could revisit this behavior if you really think it's in need of change (and if it's feasible to change the behavior).
It would be convenient, but now that I understand the issue, to me this is just a docs improvement.
For context, I could manage it with static but it didn't occur to me that the field needed to be static by design, the docs didn't help me much and I even checked the code in this repo, but without an implementation of the RandomNumberExtension I could not confirm if this was expected. The implementation added in the comment could be added for reference.
On another read through and knowing more of the internals I now understand this line in https://junit.org/junit5/docs/current/user-guide/#extensions-registration-programmatic-static-fields Extensions registered via static fields may therefore implement class-level and instance-level extension APIs such as BeforeAllCallback.
If you would like us to be able to process this issue, please provide the requested information. If the information is not provided within the next 3 weeks, we will be unable to proceed and this issue will be closed.
Team Decision: Publish the code from https://github.com/junit-team/junit5/issues/3004#issuecomment-1215045050 as part of the documentation project and link to it from the User Guide with a disclaimer that it's an example rather than the definitive way of generating random numbers for tests.
@abelsromero, does this new section of the User Guide meet your needs?
Awesome, thanks!
Awesome, thanks!
Glad to hear it. Thanks for providing feedback.