From 1153bc302b5e2957ceac7845fde32860e52e6347 Mon Sep 17 00:00:00 2001 From: Jorge Bescos Gascon Date: Tue, 23 Apr 2024 15:30:58 +0200 Subject: [PATCH] Support testng Signed-off-by: Jorge Bescos Gascon --- microprofile/testing/testng/pom.xml | 5 ++ .../testing/testng/HelidonTestNgListener.java | 65 ++++++++++++++- .../microprofile/testing/testng/MockBean.java | 32 ++++++++ .../testng/src/main/java/module-info.java | 3 +- microprofile/tests/testing/testng/pom.xml | 5 ++ .../testing/testng/TestMockBeanField.java | 82 +++++++++++++++++++ 6 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 microprofile/testing/testng/src/main/java/io/helidon/microprofile/testing/testng/MockBean.java create mode 100644 microprofile/tests/testing/testng/src/test/java/io/helidon/microprofile/tests/testing/testng/TestMockBeanField.java 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 aClass : BEAN_DEFINING.keySet()) { @@ -617,4 +669,15 @@ public Class 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"; + } + + } +}