Skip to content

Commit

Permalink
@ExplicitParameterInjection is now by default inherited for nested cl…
Browse files Browse the repository at this point in the history
…asses
  • Loading branch information
manovotn committed Jan 19, 2024
1 parent 3b7c5a6 commit 92773d5
Show file tree
Hide file tree
Showing 9 changed files with 264 additions and 26 deletions.
19 changes: 11 additions & 8 deletions junit5/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ class ContextsActivatedTest {
Sometimes you might need to add a mock for a bean that cannot be part of the test deployment, e.g. the original bean implementation has dependencies which cannot be satisfied in the test environment.
Very often, it's an ideal use case for mocking libraries, i.e. to create a bean instance with the desired behavior.
In this case, there are two options.
The first option is to add a [producer method](https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3.0.html#producer_method) or [field](https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3.0.html#producer_field) to the test class and add the test class to the deployment.
The first option is to add a [producer method](https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html#producer_method) or [field](https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html#producer_field) to the test class and add the test class to the deployment.
The test class will be recognized as a bean and therefore the producer will also be discovered.

```java
Expand Down Expand Up @@ -275,7 +275,7 @@ class TestClassProducerTest {
}
```

This should work in most of the cases (assuming the test class [meets some conditions](https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3.0.html#what_classes_are_beans)) although it's a little bit cumbersome.
This should work in most of the cases (assuming the test class [meets some conditions](https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html#what_classes_are_beans)) although it's a little bit cumbersome.
The second option is `WeldInitiator.Builder.addBeans(Bean<?>...)` which makes it possible to add beans during `AfterBeanDiscovery` phase easily.
You can provide your own `jakarta.enterprise.inject.spi.Bean` implementation or, for most use cases, a convenient `org.jboss.weld.junit.MockBean` should be sufficient.
Use `org.jboss.weld.junit.MockBean.builder()` to obtain a new builder instance.
Expand Down Expand Up @@ -493,7 +493,7 @@ To use this approach, annotate your test class with `@ExtendWith(WeldJunit5AutoE
By default, the extension will:

* Inspect your test class and try to figure out what bean classes it needs based on injection points (field and parameter injection both work)
* This is done by finding classes and verifying whether they have a [bean defining annotation](https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3.0.html#bean_defining_annotations) so make sure they do
* This is done by finding classes and verifying whether they have a [bean defining annotation](https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html#bean_defining_annotations) so make sure they do
* Add those classes to the Weld container
* Process additional annotations on the test class and also on each discovered class
* `@AddPackages`, `@AddExtensions`, `@ActivateScopes`, ...
Expand Down Expand Up @@ -650,9 +650,10 @@ This is mainly for usability, as it would be annoying to constantly type additio

However, we are aware that this might cause trouble if more extensions are competing for parameter resolution.
In such case, you can turn on explicit parameter resolution and Weld will only resolve parameters which have at least one `jakarta.inject.Qualifier` annotation on them.
There are two ways to enable it; firstly, you can do it globally, through system property - `org.jboss.weld.junit5.explicitParamInjection=true`
This property is also available as a constant in our extension class, e.g. you can use `org.jboss.weld.junit5.WeldJunit5Extension.GLOBAL_EXPLICIT_PARAM_INJECTION`.
Secondly, you can use `@ExplicitParamInjection` on your method, or test class.
There are two ways to enable it:
* First option is enabling this globally through a system property - `org.jboss.weld.junit5.explicitParamInjection=true`
This property is also available as a constant in our extension class; you can therefore refer to it via `org.jboss.weld.junit5.WeldJunit5Extension.GLOBAL_EXPLICIT_PARAM_INJECTION`.
* The other approach is to use `@ExplicitParamInjection(boolean)` on either test method, or test class.
In case of test class this annotation will enforce the presence on qualifiers on all methods.

Let's have a look at it:
Expand All @@ -675,7 +676,9 @@ class ExplicitParamInjectionTest {
```

As you might know, if you want to inject a bean where you would normally not use any qualifier, you can do that using `@Default` qualifier (as shown in the code above).
This is in accordance with the CDI specification, feel free to [read more about it](https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3.0.html#builtin_qualifiers).
This is in accordance with the CDI specification, feel free to [read more about it](https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html#builtin_qualifiers).

Last but not least, nested classes will automatically inherit this behavior from their enclosing class. They are however free to override this by declaring the annotation and its respective value themselves.

### Flat Deployment

Expand All @@ -687,4 +690,4 @@ Note that this configuration only makes a difference if you run with *enabled di

## Limitations

* `@Produces`, `@Disposes`, and `@Observes` don't work in `@Nested` test classes which fail to meet [valid bean](https://jakarta.ee/specifications/cdi/3.0/jakarta-cdi-spec-3.0.html#what_classes_are_beans) requirements due to the lack of a no-arg constructor and Weld ignores them silently. However, `@Inject` and parameter injection also work with `@Nested` classes.
* `@Produces`, `@Disposes`, and `@Observes` don't work in `@Nested` test classes which fail to meet [valid bean](https://jakarta.ee/specifications/cdi/4.0/jakarta-cdi-spec-4.0.html#what_classes_are_beans) requirements due to the lack of a no-arg constructor and Weld ignores them silently. However, `@Inject` and parameter injection also work with `@Nested` classes.
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,26 @@
import jakarta.enterprise.inject.Default;

/**
* An annotation used to enforce explicit parameter annotation. When applied, Weld will only attempt to resolve method
* parameters which have qualifiers. In case no qualifier is required for your bean, add the {@link Default} qualifier, see CDI
* specification for in depth explanation on qualifiers.
* An annotation used to enforce explicit parameter annotation. When applied and set to {@code true}, Weld will only attempt to
* resolve method parameters which have qualifiers. In case no qualifier is required for your bean, add the {@link Default}
* qualifier, see CDI specification for in depth explanation on qualifiers.
*
* This annotation can be applied either on test class, in which case it affects parameter injection in all methods, or on
* a method.
* This annotation can be applied either on a test class, in which case it affects parameter injection in all methods, or on
* a test method.
*
* Nested classes inherit the behavior declared by their enclosing class but can re-declare this annotation along with the
* {@link #value()} parameter to override the behavior.
*
* @author <a href="mailto:manovotn@redhat.com">Matej Novotny</a>
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface ExplicitParamInjection {

/**
* If set to {@code true}, Weld will only attempt to resolve parameters which have CDI qualifier annotations.
*
* @return {@code true} by default; can be explicitly set to {@code false} to make Weld attempt to resolve all parameters
*/
boolean value() default true;

}
Original file line number Diff line number Diff line change
Expand Up @@ -103,13 +103,22 @@ private static void storeExplicitParamResolutionInformation(ExtensionContext ec)
return;
}
// check class-level annotation
for (Annotation annotation : ec.getRequiredTestClass().getAnnotations()) {
if (annotation.annotationType().equals(ExplicitParamInjection.class)) {
setExplicitInjectionInfoToStore(ec, true);
break;
Class<?> inspectedTestClass = ec.getRequiredTestClass();
ExplicitParamInjection explicitParamInjection = inspectedTestClass.getAnnotation(ExplicitParamInjection.class);
if (explicitParamInjection != null) {
setExplicitInjectionInfoToStore(ec, explicitParamInjection.value());
} else {
// if not found, it can still be a nested class
// inspect enclosing classes until first annotation is found or until we hit top-level class
inspectedTestClass = inspectedTestClass.getEnclosingClass();
while (inspectedTestClass != null && explicitParamInjection == null) {
explicitParamInjection = inspectedTestClass.getAnnotation(ExplicitParamInjection.class);
if (explicitParamInjection != null) {
setExplicitInjectionInfoToStore(ec, explicitParamInjection.value());
}
inspectedTestClass = inspectedTestClass.getEnclosingClass();
}
}

}

@Override
Expand Down Expand Up @@ -237,10 +246,9 @@ private List<Annotation> resolveQualifiers(ParameterContext pc, BeanManager bm)
}

private boolean methodRequiresExplicitParamInjection(ParameterContext pc) {
for (Annotation annotation : pc.getDeclaringExecutable().getAnnotations()) {
if (annotation.annotationType().equals(ExplicitParamInjection.class)) {
return true;
}
ExplicitParamInjection ann = pc.getDeclaringExecutable().getAnnotation(ExplicitParamInjection.class);
if (ann != null) {
return ann.value();
}
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;

import org.jboss.weld.junit5.basic.Foo;
import org.jboss.weld.junit5.explicitInjection.Bar;
Expand All @@ -16,7 +15,7 @@ public class ParametersAutoConfigTest {
@DisplayName("Ensure the parameters Foo and Bar are automatically included in container with no configuration")
void test(Foo foo, Bar bar) {
assertNotNull(bar);
assertNull(bar.ping());
assertEquals(Bar.class.getSimpleName(), bar.ping());
assertNotNull(foo);
assertEquals(foo.getBar(), "baz");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public class Bar {
private String someText = null;

public Bar() {
this.someText = Bar.class.getSimpleName();
}

public Bar(String someText) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ public class CustomExtension implements ParameterResolver {
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
// dumb approach but we only ever resolve Bar anyway :)
return new Bar(CustomExtension.class.getSimpleName());
if (parameterContext.getParameter().getType().equals(Bar.class)) {
return new Bar(CustomExtension.class.getSimpleName());
} else {
throw new IllegalStateException(getClass().getName() + " can only resolve parameter Bar!");
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.jboss.weld.junit5.explicitInjection;

import jakarta.enterprise.inject.Default;

import org.jboss.weld.junit5.ExplicitParamInjection;
import org.jboss.weld.junit5.WeldJunit5Extension;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(WeldJunit5Extension.class)
@ExplicitParamInjection(false)
public class ExplicitParameterInjectionNestedClass2Test {

@Test
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by Weld
Assertions.assertNotNull(bar);
Assertions.assertEquals(Bar.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}

@Nested
class NestedTestClass {

@Test
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by Weld
Assertions.assertNotNull(bar);
Assertions.assertEquals(Bar.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}

@Nested
class TwiceNestedTestClass1 {

@Test
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by Weld
Assertions.assertNotNull(bar);
Assertions.assertEquals(Bar.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}
}

@Nested
@ExplicitParamInjection(true)
@ExtendWith(CustomExtension.class) // TwiceNestedTestClass2 and ThriceNestedClass will both use this
class TwiceNestedTestClass2 {

@Test
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by another extension
Assertions.assertNotNull(bar);
Assertions.assertEquals(CustomExtension.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}

@Nested
class ThriceNestedClass {

@Test
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by another extension
Assertions.assertNotNull(bar);
Assertions.assertEquals(CustomExtension.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package org.jboss.weld.junit5.explicitInjection;

import jakarta.enterprise.inject.Default;

import org.jboss.weld.junit5.ExplicitParamInjection;
import org.jboss.weld.junit5.WeldJunit5Extension;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

// Note that @ExtendWith(CustomExtension.class) has to be on each method separately. The inheritance of this would
// otherwise cause failures for TwiceNestedTestClass2 and ThriceNestedClass where Weld claims all parameters.
@ExtendWith(WeldJunit5Extension.class)
@ExplicitParamInjection(true)
public class ExplicitParameterInjectionNestedClassTest {

@Test
@ExtendWith(CustomExtension.class)
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by another extension
Assertions.assertNotNull(bar);
Assertions.assertEquals(CustomExtension.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}

@Nested
class NestedTestClass {

@Test
@ExtendWith(CustomExtension.class)
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by another extension
Assertions.assertNotNull(bar);
Assertions.assertEquals(CustomExtension.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}

@Nested
class TwiceNestedTestClass1 {

@Test
@ExtendWith(CustomExtension.class)
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by another extension
Assertions.assertNotNull(bar);
Assertions.assertEquals(CustomExtension.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}
}

@Nested
@ExplicitParamInjection(false)
class TwiceNestedTestClass2 {

@Test
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by Weld
Assertions.assertNotNull(bar);
Assertions.assertEquals(Bar.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}

@Nested
class ThriceNestedClass {

@Test
public void testParameterResolution(@Default Foo foo, Bar bar, @MyQualifier BeanWithQualifier bean) {
// Bar should be resolved by Weld
Assertions.assertNotNull(bar);
Assertions.assertEquals(Bar.class.getSimpleName(), bar.ping());
// Foo should be resolved as usual
Assertions.assertNotNull(foo);
Assertions.assertEquals(Foo.class.getSimpleName(), foo.ping());
// BeanWithQualifier should be resolved
Assertions.assertNotNull(bean);
Assertions.assertEquals(BeanWithQualifier.class.getSimpleName(), bean.ping());
}
}
}
}
}
Loading

0 comments on commit 92773d5

Please sign in to comment.