diff --git a/microprofile/testing/testng/pom.xml b/microprofile/testing/testng/pom.xml
index 1fe04815c11..e4a86f29769 100644
--- a/microprofile/testing/testng/pom.xml
+++ b/microprofile/testing/testng/pom.xml
@@ -44,6 +44,11 @@
helidon-microprofile-cdi
provided
+
+ org.mockito
+ mockito-core
+ provided
+
org.glassfish.jersey.ext.cdi
jersey-weld2-se
diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java
index e2ddb7f3fc8..dc34e6a61d1 100644
--- a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java
+++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/HelidonTestNgListener.java
@@ -27,6 +27,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.Set;
@@ -44,13 +45,17 @@
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.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;
@@ -67,6 +72,8 @@
import org.testng.ITestListener;
import org.testng.ITestResult;
import org.testng.annotations.Test;
+import org.mockito.MockSettings;
+import org.mockito.Mockito;
/**
* TestNG extension to support Helidon CDI container in tests.
@@ -301,6 +308,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");
}
}
@@ -311,6 +323,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");
}
}
}
@@ -419,6 +436,7 @@ private T[] getAnnotations(Class> testClass, Class a
private static class AddBeansExtension implements Extension {
private final Class> testClass;
private final List addBeans;
+ private final Set> mocks = new HashSet<>();
private AddBeansExtension(Class> testClass, List addBeans) {
this.testClass = testClass;
@@ -426,7 +444,7 @@ private AddBeansExtension(Class> testClass, List addBeans) {
}
@SuppressWarnings("unchecked")
- void registerOtherBeans(@Observes AfterBeanDiscovery event) {
+ void registerOtherBeans(@Observes AfterBeanDiscovery event, BeanManager beanManager) {
Client client = ClientBuilder.newClient();
event.addBean()
@@ -445,6 +463,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> 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) {
@@ -471,6 +509,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()) {
@@ -617,4 +669,15 @@ public Class extends Extension> value() {
}
}
+ /**
+ * Supports inline instantiation of the {@link Inject} annotation.
+ */
+ private static final class Literal extends AnnotationLiteral implements Inject {
+
+ private static final Literal INSTANCE = new Literal();
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ }
}
diff --git a/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/MockBean.java b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/MockBean.java
new file mode 100644
index 00000000000..b1099186f37
--- /dev/null
+++ b/microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/MockBean.java
@@ -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 {
+
+}
diff --git a/microprofile/testing/testng/src/main/java/module-info.java b/microprofile/testing/testng/src/main/java/module-info.java
index c2e60fd39aa..2595550be5e 100644
--- a/microprofile/testing/testng/src/main/java/module-info.java
+++ b/microprofile/testing/testng/src/main/java/module-info.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 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.
@@ -29,6 +29,7 @@
requires jakarta.ws.rs;
requires microprofile.config.api;
requires org.testng;
+ requires org.mockito;
requires static io.helidon.microprofile.server;
requires static jersey.cdi1x;
diff --git a/microprofile/tests/testing/testng/pom.xml b/microprofile/tests/testing/testng/pom.xml
index ed741b47056..77cf12cd414 100644
--- a/microprofile/tests/testing/testng/pom.xml
+++ b/microprofile/tests/testing/testng/pom.xml
@@ -54,5 +54,10 @@
hamcrest-core
test
+
+ org.mockito
+ mockito-core
+ test
+
diff --git a/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanField.java b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanField.java
new file mode 100644
index 00000000000..0f56cb6c80e
--- /dev/null
+++ b/microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanField.java
@@ -0,0 +1,82 @@
+/*
+ * 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.tests.testing.testng;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import jakarta.enterprise.inject.Produces;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.WebTarget;
+
+import io.helidon.microprofile.testing.testng.AddBean;
+import io.helidon.microprofile.testing.testng.HelidonTest;
+import io.helidon.microprofile.testing.testng.MockBean;
+
+import org.mockito.Answers;
+import org.mockito.MockSettings;
+import org.mockito.Mockito;
+import org.testng.annotations.Test;
+
+@HelidonTest
+@AddBean(TestMockBeanField.Resource.class)
+@AddBean(TestMockBeanField.Service.class)
+class TestMockBeanField {
+
+ @Inject
+ @MockBean
+ private Service service;
+ @Inject
+ private WebTarget target;
+
+ @Test
+ void injectionTest() {
+ String response = target.path("/test").request().get(String.class);
+ // Defaults to specified in @Produces
+ assertThat(response, is("Not Mocked"));
+ Mockito.when(service.test()).thenReturn("Mocked");
+ response = target.path("/test").request().get(String.class);
+ assertThat(response, is("Mocked"));
+ }
+
+ @Produces
+ MockSettings mockSettings() {
+ return Mockito.withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS);
+ }
+
+ @Path("/test")
+ public static class Resource {
+
+ @Inject
+ private Service service;
+
+ @GET
+ public String test() {
+ return service.test();
+ }
+ }
+
+ static class Service {
+
+ String test() {
+ return "Not Mocked";
+ }
+
+ }
+}