diff --git a/src/main/java/org/mockito/testng/MockitoSettings.java b/src/main/java/org/mockito/testng/MockitoSettings.java new file mode 100644 index 0000000..50b0a7e --- /dev/null +++ b/src/main/java/org/mockito/testng/MockitoSettings.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockito.testng; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.mockito.quality.Strictness; + +/** + * Annotation that can configure Mockito settings. Used by {@link MockitoTestNGListener} + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Documented +public @interface MockitoSettings { + + /** + * Configure the strictness used in this test. + * + * @return The strictness to configure, by default {@link Strictness#STRICT_STUBS} + */ + Strictness strictness() default Strictness.STRICT_STUBS; +} diff --git a/src/main/java/org/mockito/testng/MockitoTestNGListener.java b/src/main/java/org/mockito/testng/MockitoTestNGListener.java index 1a76cf2..067b6f5 100644 --- a/src/main/java/org/mockito/testng/MockitoTestNGListener.java +++ b/src/main/java/org/mockito/testng/MockitoTestNGListener.java @@ -4,16 +4,19 @@ */ package org.mockito.testng; +import java.lang.annotation.Annotation; +import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.WeakHashMap; +import java.util.stream.Stream; import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; import org.testng.IInvokedMethod; import org.testng.IInvokedMethodListener; -import org.testng.ITestNGListener; +import org.testng.ITestNGMethod; import org.testng.ITestResult; import org.testng.annotations.Listeners; @@ -55,6 +58,19 @@ * * *

+ * By default {@link MockitoSession} is started with {@link Strictness#STRICT_STUBS}. + * You can change this behavior by adding {@link MockitoSettings} to your test class. + *

+ * + *

+ * @Listeners(MockitoTestNGListener.class)
+ * @MockitoSettings(strictness = Strictness.WARN)
+ * public class ExampleTest {
+ *  ...
+ * }
+ * 
+ * + *

* MockitoTestNGListener not working with parallel tests, * more information https://github.com/mockito/mockito-testng/issues/20 *

@@ -65,49 +81,65 @@ public class MockitoTestNGListener implements IInvokedMethodListener { @Override public void beforeInvocation(IInvokedMethod method, ITestResult testResult) { - if (hasMockitoTestNGListenerInTestHierarchy(testResult)) { - sessions.computeIfAbsent(testResult.getInstance(), testInstance -> - Mockito.mockitoSession() - .initMocks(testInstance) - .strictness(Strictness.STRICT_STUBS) - .startMocking() + if (shouldBeRunBeforeInvocation(method, testResult)) { + sessions.computeIfAbsent(testResult.getInstance(), testInstance -> { + + Strictness strictness = findAnnotation(testResult, MockitoSettings.class) + .map(MockitoSettings::strictness).orElse(Strictness.STRICT_STUBS); + + return Mockito.mockitoSession() + .initMocks(testInstance) + .strictness(strictness) + .startMocking(); + } ); } } @Override public void afterInvocation(IInvokedMethod method, ITestResult testResult) { - if (hasMockitoTestNGListenerInTestHierarchy(testResult) && method.isTestMethod()) { + if (shouldBeRunAfterInvocation(method, testResult)) { Optional.ofNullable(sessions.remove(testResult.getInstance())) .ifPresent(mockitoSession -> mockitoSession.finishMocking(testResult.getThrowable())); } } - protected boolean hasMockitoTestNGListenerInTestHierarchy(ITestResult testResult) { - for (Class clazz = testResult.getTestClass().getRealClass(); clazz != Object.class; clazz = clazz.getSuperclass()) { - if (hasMockitoTestNGListener(clazz)) { - return true; - } - } - return false; + private boolean shouldBeRunBeforeInvocation(IInvokedMethod method, ITestResult testResult) { + return !isAfterConfigurationMethod(method) && hasMockitoTestNGListener(testResult); } - protected boolean hasMockitoTestNGListener(Class clazz) { - Listeners listeners = clazz.getAnnotation(Listeners.class); - if (listeners == null) { - return false; - } + private boolean isAfterConfigurationMethod(IInvokedMethod method) { + ITestNGMethod testMethod = method.getTestMethod(); + return testMethod.isAfterClassConfiguration() + || testMethod.isAfterMethodConfiguration() + || testMethod.isAfterGroupsConfiguration() + || testMethod.isAfterTestConfiguration() + || testMethod.isAfterSuiteConfiguration(); + } - for (Class listenerClass : listeners.value()) { - if (listenerClass() == listenerClass) { - return true; - } - } - return false; + private boolean shouldBeRunAfterInvocation(IInvokedMethod method, ITestResult testResult) { + return method.isTestMethod() && hasMockitoTestNGListener(testResult); } - protected Class listenerClass() { - return MockitoTestNGListener.class; + protected boolean hasMockitoTestNGListener(ITestResult testResult) { + + return findAnnotation(testResult, Listeners.class) + .map(Listeners::value) + .map(Arrays::stream) + .orElseGet(Stream::empty) + .anyMatch(listener -> listener == MockitoTestNGListener.class); } + Optional findAnnotation(ITestResult testResult, Class annotationClass) { + + for (Class clazz = testResult.getTestClass().getRealClass(); + clazz != Object.class; clazz = clazz.getSuperclass()) { + Optional annotation = Optional.ofNullable(clazz.getAnnotation(annotationClass)); + if (annotation.isPresent()) { + return annotation; + } + } + + return Optional.empty(); + } } diff --git a/src/test/java/org/mockitousage/testng/InjectMocksWithConstructorAndFinalTest.java b/src/test/java/org/mockitousage/testng/InjectMocksWithConstructorAndFinalTest.java new file mode 100644 index 0000000..ebc1a60 --- /dev/null +++ b/src/test/java/org/mockitousage/testng/InjectMocksWithConstructorAndFinalTest.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitousage.testng; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Random; + +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class InjectMocksWithConstructorAndFinalTest { + + interface Client { + int aMethod(); + } + + static class Service { + final Client client; + + public Service(Client client) { + this.client = client; + } + + int callClient() { + return client.aMethod(); + } + } + + @Mock + private Client client; + + @InjectMocks + private Service service; + + private int lastClientHash = 0; + + @AfterMethod + void cleanup() { + // final field will be not assigned for next test + // for new object constructor will be used + service = null; + } + + @Test(invocationCount = 4) + void inject_mock_should_be_refreshed() { + + // we have correct injected mock + assertThat(client).isNotNull(); + assertThat(service.client).isNotNull().isSameAs(client); + + // we have new mock + assertThat(lastClientHash).isNotEqualTo(client.hashCode()); + + // clear mock + assertThat(service.callClient()).isZero(); + + // make some stub + int i = new Random().nextInt() + 1; + when(client.aMethod()).thenReturn(i); + + // and test it + assertThat(service.callClient()).isEqualTo(i); + + verify(client, times(2)).aMethod(); + + // remember last mock hash + lastClientHash = client.hashCode(); + } +} diff --git a/src/test/java/org/mockitousage/testng/StrictStubsTest.java b/src/test/java/org/mockitousage/testng/StrictStubsTest.java index 80c3b31..6d0df62 100644 --- a/src/test/java/org/mockitousage/testng/StrictStubsTest.java +++ b/src/test/java/org/mockitousage/testng/StrictStubsTest.java @@ -14,6 +14,7 @@ import org.mockito.Mock; import org.mockito.exceptions.misusing.PotentialStubbingProblem; import org.mockito.exceptions.misusing.UnnecessaryStubbingException; +import org.mockito.testng.MockitoSettings; import org.mockito.testng.MockitoTestNGListener; import org.mockitousage.testng.failuretests.HasUnusedStubs; import org.mockitousage.testng.failuretests.HasUnusedStubsInSetup; @@ -22,6 +23,8 @@ import org.testng.annotations.Test; @Listeners(MockitoTestNGListener.class) +// MockitoSettings with default values +@MockitoSettings public class StrictStubsTest { @Mock diff --git a/src/test/java/org/mockitousage/testng/StrictnessWarnTest.java b/src/test/java/org/mockitousage/testng/StrictnessWarnTest.java new file mode 100644 index 0000000..8f54ccf --- /dev/null +++ b/src/test/java/org/mockitousage/testng/StrictnessWarnTest.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2020 Mockito contributors + * This program is made available under the terms of the MIT License. + */ +package org.mockitousage.testng; + +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.mockito.Mock; +import org.mockito.quality.Strictness; +import org.mockito.testng.MockitoSettings; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +@MockitoSettings(strictness = Strictness.WARN) +public class StrictnessWarnTest { + + @Mock + private List list; + + @BeforeMethod + void setup() { + when(list.add("a")).thenReturn(true); + } + + @Test + void not_used_stub_generate_warn() { + // + } +} diff --git a/version.properties b/version.properties index 96ff41b..67c6069 100644 --- a/version.properties +++ b/version.properties @@ -1 +1 @@ -version=0.1.* +version=0.2.*