junit5 icon indicating copy to clipboard operation
junit5 copied to clipboard

Extension registration does not work for non static fields

Open abelsromero opened this issue 3 years ago • 2 comments

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

  • [ ] ...

abelsromero avatar Aug 15 '22 13:08 abelsromero

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.

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);
			}
		});
	}

}

sbrannen avatar Aug 15 '22 13:08 sbrannen

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.

abelsromero avatar Aug 15 '22 20:08 abelsromero

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.

stale[bot] avatar Apr 11 '23 20:04 stale[bot]

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.

marcphilipp avatar Apr 12 '23 08:04 marcphilipp

@abelsromero, does this new section of the User Guide meet your needs?

sbrannen avatar Jun 16 '23 15:06 sbrannen

Awesome, thanks!

abelsromero avatar Jun 16 '23 17:06 abelsromero

Awesome, thanks!

Glad to hear it. Thanks for providing feedback.

sbrannen avatar Jun 16 '23 17:06 sbrannen