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

Make annotations usable as meta annotations #223

Open
Adrodoc55 opened this issue Oct 21, 2024 · 2 comments
Open

Make annotations usable as meta annotations #223

Adrodoc55 opened this issue Oct 21, 2024 · 2 comments

Comments

@Adrodoc55
Copy link

It would be great if annotations such as @ExcludeBean would add @Target(ElementType.ANNOTATION_TYPE) to allow their usage as meta annotations.
This is a feature that JUnit-Jupiter offers for pretty much all their annotations and I find it really useful: https://junit.org/junit5/docs/current/user-guide/#writing-tests-meta-annotations
You wouldn't even have to change anything other than the annotation target, because you already use JUnit-Jupiters AnnotationSupport:

AnnotationSupport.findAnnotatedFields(currClass, ExcludeBean.class).stream()
.map(Field::getType)
.forEach(excludedBeanTypes::add);

Motivating Example

For example I have a test that uses @EnableAutoWeld and @ExtendWith(MockitoExtension.class) and to produce a mock in this test I have a field like this:

private @ExcludeBean @Produces @ApplicationScoped @Mock MyClass myMockedInstance;

If I could use @ExcludeBean as a meta-annotation, I would be able to define a custom annotation @CdiMock and reduce the field declaration to the following:

private @CdiMock MyClass myMockedInstance;

Now currently I can't quite achieve this, because @Mock has the same issue as @ExcludeBean, but I opened a ticket there as well: mockito/mockito#3482

@Produces and @ApplicationScoped on the other hand can easily be added automatically for CdiMock fields by subclassing WeldJunit5AutoExtension and adding the following CDI extension in weldInit:

import static java.util.Objects.requireNonNull;
import java.lang.reflect.Field;
import java.lang.reflect.UndeclaredThrowableException;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.platform.commons.support.AnnotationSupport;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.Extension;

public class CdiExtension implements Extension {
  private final TestInstances testInstances;

  public CdiExtension(TestInstances testInstances) {
    this.testInstances = requireNonNull(testInstances, "testInstances");
  }

  @SuppressWarnings("unused")
  private void afterBeanDiscovery(@Observes AfterBeanDiscovery event, BeanManager beanManager) {
    for (Object testInstance : testInstances.getAllInstances()) {
      for (Field field : AnnotationSupport.findAnnotatedFields(testInstance.getClass(), CdiMock.class)) {
        event.addBean() //
            .addType(field.getGenericType()) //
            .scope(ApplicationScoped.class) //
            .produceWith(it -> getFieldValue(testInstance, field));
      }
    }
  }

  private static Object getFieldValue(Object object, Field field) {
    boolean wasAccessible = field.canAccess(object);
    if (!wasAccessible) {
      field.setAccessible(true);
    }
    try {
      return field.get(object);
    } catch (IllegalAccessException ex) {
      throw new UndeclaredThrowableException(ex);
    } finally {
      if (!wasAccessible) {
        field.setAccessible(false);
      }
    }
  }
}
@manovotn
Copy link
Collaborator

manovotn commented Oct 22, 2024

Hello, bellow are my thoughts/observations.

You wouldn't even have to change anything other than the annotation target, because you already use JUnit-Jupiters AnnotationSupport:

We use that only for scanning - basically determining what classes to register into a synthetic bean archive for given test.
However, this does not "discover" a producer method/field as such; that part is left to the CDI container (Weld Core).
Weld-junit just registers the test class as additional bean (which is done here) for Weld core to process.
And the catch here is that CDI container will not scan all bean fields/methods for all annotations and then try to scan those for other possibly CDI-related annotations (with the exception of CDI @Stereotype).

If I could use @ExcludeBean as a meta-annotation, I would be able to define a custom annotation @Cdimock and reduce the field declaration to the following:

This is not true I am afraid; the CDI annotation @Produces is only applicable to to methods and fields.
So you couldn't add it onto arbitrary annotation.

@produces and @ApplicationScoped on the other hand can easily be added automatically for CdiMock fields by subclassing WeldJunit5AutoExtension and adding the following CDI extension in weldInit:

Right, this is a workaround for what I stated as the problem with the discovery of a producer method. You basically circumvent that by declaring such bean to be a synthetic bean with value coming from the [mocked] field.

With all of the above, I am not sure I understand what exactly you ask for. If you are willing to keep your custom CdiExtension applied via subclass of WeldJunit5AutoExtension, then you needn't even put bean scope and @Produces anywhere as the CDI extension basically does that for you anyway. All you'd need is for @ExcludeBean and @Mock to be applicable to another annotation (and that's is fine and would easily work).
If you, however, hoped to be rid of the extension and just have your meta annotation, then I am afraid that approach won't really work.

@manovotn
Copy link
Collaborator

@Adrodoc55 could you please clarify your use case WRT my comment above?

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

No branches or pull requests

2 participants