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

4.x: Ability to Inject MockBeans in Helidon #7694 #8674

Merged
merged 13 commits into from
May 17, 2024
5 changes: 5 additions & 0 deletions microprofile/testing/junit5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
<artifactId>junit-jupiter-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
romain-grecourt marked this conversation as resolved.
Show resolved Hide resolved
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.helidon.jersey</groupId>
<artifactId>helidon-jersey-client</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
Expand All @@ -48,11 +49,16 @@
import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.se.SeContainerInitializer;
import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
import jakarta.enterprise.inject.spi.AnnotatedParameter;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.BeforeBeanDiscovery;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.Extension;
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.enterprise.inject.spi.ProcessAnnotatedType;
import jakarta.enterprise.inject.spi.ProcessInjectionPoint;
import jakarta.enterprise.inject.spi.WithAnnotations;
import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator;
import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Inject;
Expand All @@ -77,6 +83,8 @@
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.ReflectiveInvocationContext;
import org.mockito.MockSettings;
import org.mockito.Mockito;


/**
Expand Down Expand Up @@ -294,6 +302,11 @@ private void validatePerTest() {
+ " injection into fields or constructor is not supported, as each"
+ " test method uses a different CDI container. Field " + field
+ " is annotated with @Inject");
} else if (field.getAnnotation(MockBean.class) != null) {
throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true),"
+ " injection into fields or constructor is not supported, as each"
+ " test method uses a different CDI container. Field " + field
+ " is annotated with @MockBean");
}
}

Expand All @@ -304,6 +317,11 @@ private void validatePerTest() {
+ " injection into fields or constructor is not supported, as each"
+ " test method uses a different CDI container. Field " + field
+ " is annotated with @Inject");
} else if (field.getAnnotation(MockBean.class) != null) {
throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true),"
+ " injection into fields or constructor is not supported, as each"
+ " test method uses a different CDI container. Field " + field
+ " is annotated with @MockBean");
}
}
}
Expand Down Expand Up @@ -530,13 +548,13 @@ private static class AddBeansExtension implements Extension {
private final List<AddBean> addBeans;

private final HashMap<String, Annotation> socketAnnotations = new HashMap<>();
private final Set<Class<?>> mocks = new HashSet<>();

private AddBeansExtension(Class<?> testClass, List<AddBean> addBeans) {
this.testClass = testClass;
this.addBeans = addBeans;
}


void processSocketInjectionPoints(@Observes ProcessInjectionPoint<?, WebTarget> event) throws Exception{
InjectionPoint injectionPoint = event.getInjectionPoint();
Set<Annotation> qualifiers = injectionPoint.getQualifiers();
Expand All @@ -547,10 +565,36 @@ void processSocketInjectionPoints(@Observes ProcessInjectionPoint<?, WebTarget>
break;
}
}
}

void processMockBean(@Observes @WithAnnotations(MockBean.class) ProcessAnnotatedType<?> obj) throws Exception {
var configurator = obj.configureAnnotatedType();
configurator.fields().forEach(field -> {
MockBean mockBean = field.getAnnotated().getAnnotation(MockBean.class);
if (mockBean != null) {
Field f = field.getAnnotated().getJavaMember();
// Adds @Inject to be more user friendly
field.add(Literal.INSTANCE);
Class<?> fieldType = f.getType();
mocks.add(fieldType);
}
});
configurator.constructors().forEach(constructor -> {
processMockBeanParameters(constructor.getAnnotated().getParameters());
});
}

private void processMockBeanParameters(List<? extends AnnotatedParameter<?>> parameters) {
parameters.stream().forEach(parameter -> {
MockBean mockBean = parameter.getAnnotation(MockBean.class);
if (mockBean != null) {
Class<?> parameterType = parameter.getJavaParameter().getType();
mocks.add(parameterType);
}
});
}

void registerOtherBeans(@Observes AfterBeanDiscovery event) {
void registerOtherBeans(@Observes AfterBeanDiscovery event, BeanManager beanManager) {

Client client = ClientBuilder.newClient();

Expand All @@ -569,6 +613,25 @@ void registerOtherBeans(@Observes AfterBeanDiscovery event) {
.scope(ApplicationScoped.class)
.createWith(context -> getWebTarget(client, "@default"));

// Register all mocks
mocks.forEach(type -> {
event.addBean()
.addType(type)
.scope(ApplicationScoped.class)
.alternative(true)
.createWith(inst -> {
Set<Bean<?>> beans = beanManager.getBeans(MockSettings.class);
romain-grecourt marked this conversation as resolved.
Show resolved Hide resolved
if (!beans.isEmpty()) {
Bean<?> bean = beans.iterator().next();
MockSettings mockSettings = (MockSettings) beanManager.getReference(bean, MockSettings.class,
beanManager.createCreationalContext(null));
return Mockito.mock(type, mockSettings);
} else {
return Mockito.mock(type);
}
})
.priority(0);
});
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -767,4 +830,15 @@ public Class<? extends Extension> value() {
}
}

/**
* Supports inline instantiation of the {@link Inject} annotation.
*/
private static final class Literal extends AnnotationLiteral<Inject> implements Inject {

private static final Literal INSTANCE = new Literal();

@Serial
private static final long serialVersionUID = 1L;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.microprofile.testing.junit5;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* A field annotated with @MockBean will be mocked by Mockito
* and injected in every place it is referenced.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface MockBean {

}
3 changes: 2 additions & 1 deletion microprofile/testing/junit5/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2023 Oracle and/or its affiliates.
* Copyright (c) 2020, 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,6 +24,7 @@
requires io.helidon.microprofile.cdi;
requires jakarta.inject;
requires org.junit.jupiter.api;
requires org.mockito;

requires transitive jakarta.cdi;
requires transitive jakarta.ws.rs;
Expand Down
5 changes: 5 additions & 0 deletions microprofile/testing/testng/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
<artifactId>helidon-microprofile-cdi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>provided</scope>
romain-grecourt marked this conversation as resolved.
Show resolved Hide resolved
</dependency>
<dependency>
<groupId>org.glassfish.jersey.ext.cdi</groupId>
<artifactId>jersey-weld2-se</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
Expand All @@ -49,12 +50,15 @@
import jakarta.enterprise.inject.se.SeContainerInitializer;
import jakarta.enterprise.inject.spi.AfterBeanDiscovery;
import jakarta.enterprise.inject.spi.AnnotatedType;
import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.BeforeBeanDiscovery;
import jakarta.enterprise.inject.spi.CDI;
import jakarta.enterprise.inject.spi.Extension;
import jakarta.enterprise.inject.spi.InjectionTarget;
import jakarta.enterprise.inject.spi.InjectionTargetFactory;
import jakarta.enterprise.inject.spi.ProcessAnnotatedType;
import jakarta.enterprise.inject.spi.WithAnnotations;
import jakarta.enterprise.inject.spi.configurator.AnnotatedTypeConfigurator;
import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Inject;
Expand All @@ -66,6 +70,8 @@
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.glassfish.jersey.ext.cdi1x.internal.CdiComponentProvider;
import org.mockito.MockSettings;
import org.mockito.Mockito;
import org.testng.IClassListener;
import org.testng.ITestClass;
import org.testng.ITestListener;
Expand Down Expand Up @@ -307,6 +313,11 @@ private void validatePerTest() {
+ " injection into fields or constructor is not supported, as each"
+ " test method uses a different CDI container. Field " + field
+ " is annotated with @Inject");
} else if (field.getAnnotation(MockBean.class) != null) {
throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true),"
+ " injection into fields or constructor is not supported, as each"
+ " test method uses a different CDI container. Field " + field
+ " is annotated with @MockBean");
}
}

Expand All @@ -317,6 +328,11 @@ private void validatePerTest() {
+ " injection into fields or constructor is not supported, as each"
+ " test method uses a different CDI container. Field " + field
+ " is annotated with @Inject");
} else if (field.getAnnotation(MockBean.class) != null) {
throw new RuntimeException("When a class is annotated with @HelidonTest(resetPerTest=true),"
+ " injection into fields or constructor is not supported, as each"
+ " test method uses a different CDI container. Field " + field
+ " is annotated with @MockBean");
}
}
}
Expand Down Expand Up @@ -445,14 +461,15 @@ private <T extends Annotation> T[] getAnnotations(Class<?> testClass, Class<T> a
private static class AddBeansExtension implements Extension {
private final Class<?> testClass;
private final List<AddBean> addBeans;
private final Set<Class<?>> mocks = new HashSet<>();

private AddBeansExtension(Class<?> testClass, List<AddBean> addBeans) {
this.testClass = testClass;
this.addBeans = addBeans;
}

@SuppressWarnings("unchecked")
void registerOtherBeans(@Observes AfterBeanDiscovery event) {
void registerOtherBeans(@Observes AfterBeanDiscovery event, BeanManager beanManager) {
Client client = ClientBuilder.newClient();

event.addBean()
Expand All @@ -471,6 +488,26 @@ void registerOtherBeans(@Observes AfterBeanDiscovery event) {
return client.target("http://localhost:7001");
}
});

// Register all mocks
mocks.forEach(type -> {
event.addBean()
.addType(type)
.scope(ApplicationScoped.class)
.alternative(true)
.createWith(inst -> {
Set<Bean<?>> beans = beanManager.getBeans(MockSettings.class);
if (!beans.isEmpty()) {
Bean<?> bean = beans.iterator().next();
MockSettings mockSettings = (MockSettings) beanManager.getReference(bean, MockSettings.class,
beanManager.createCreationalContext(null));
return Mockito.mock(type, mockSettings);
} else {
return Mockito.mock(type);
}
})
.priority(0);
});
}

void registerAddedBeans(@Observes BeforeBeanDiscovery event) {
Expand All @@ -497,6 +534,20 @@ void registerAddedBeans(@Observes BeforeBeanDiscovery event) {
}
}

void processMockBean(@Observes @WithAnnotations(MockBean.class) ProcessAnnotatedType<?> obj) throws Exception {
var configurator = obj.configureAnnotatedType();
configurator.fields().forEach(field -> {
MockBean mockBean = field.getAnnotated().getAnnotation(MockBean.class);
if (mockBean != null) {
Field f = field.getAnnotated().getJavaMember();
// Adds @Inject to be more user friendly
field.add(Literal.INSTANCE);
Class<?> fieldType = f.getType();
mocks.add(fieldType);
}
});
}

private boolean hasBda(Class<?> value) {
// does it have bean defining annotation?
for (Class<? extends Annotation> aClass : BEAN_DEFINING.keySet()) {
Expand Down Expand Up @@ -653,4 +704,15 @@ public Class<? extends Extension> value() {
}
}

/**
* Supports inline instantiation of the {@link Inject} annotation.
*/
private static final class Literal extends AnnotationLiteral<Inject> implements Inject {
romain-grecourt marked this conversation as resolved.
Show resolved Hide resolved

private static final Literal INSTANCE = new Literal();

@Serial
private static final long serialVersionUID = 1L;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright (c) 2024 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.microprofile.testing.testng;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* A field annotated with @MockBean will be mocked by Mockito
* and injected in every place it is referenced.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface MockBean {

}
Loading