Support @⁠MockitoBean reset and MockitoSession management with @⁠Nested tests

Prior to this commit, the MockitoResetTestExecutionListener failed to
reset mocks created via @⁠MockitoBean if the @⁠MockitoBean field was
declared in an enclosing class for a @⁠Nested test class. In addition,
the MockitoSession was not properly managed by the
MockitoTestExecutionListener.

This commit addresses those issue as follows.

1) The hasMockitoAnnotations() utility method has been overhauled so
that it finds Mockito annotations not only on the current test class
and on fields of the current test class but also on interfaces,
superclasses, and enclosing classes for @⁠Nested test classes as well
as on fields of superclasses and enclosing classes.

That allows the MockitoResetTestExecutionListener to properly detect
that it needs to reset mocks for fields declared in enclosing classes
for @⁠Nested classes.

2) MockitoTestExecutionListener has been revised so that it only
initializes a MockitoSession before each test method and closes the
MockitoSession after each test method. In addition, it now only manages
the MockitoSession when hasMockitoAnnotations() returns true for the
current test class (which may be a @⁠Nested test class). Furthermore,
it no longer attempts to initialize a MockitoSession during the
prepareTestInstance() callback since that results in an
UnfinishedMockingSessionException for a @⁠Nested test class due to the
fact that a MockitoSession was already created for the current thread
for the enclosing test class.

Closes gh-33676
This commit is contained in:
Sam Brannen
2024-10-11 16:07:03 +02:00
parent 7fb6a2e4f7
commit eb4bf1c0a6
3 changed files with 151 additions and 49 deletions

View File

@@ -0,0 +1,99 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* 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
*
* https://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 org.springframework.test.context.bean.override.mockito;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.times;
/**
* Verifies proper handling of the {@link org.mockito.MockitoSession MockitoSession}
* when a {@link MockitoBean @MockitoBean} field is declared in the enclosing class of
* a {@link Nested @Nested} test class.
*
* @author Andy Wilkinson
* @author Sam Brannen
* @since 6.2
*/
@ExtendWith(SpringExtension.class)
// TODO Remove @ContextConfiguration declaration.
// @ContextConfiguration is currently required due to a bug in the TestContext framework.
@ContextConfiguration
class MockitoBeanNestedTests {
@MockitoBean
Runnable action;
@Autowired
Task task;
@Test
void mockWasInvokedOnce() {
task.execute();
then(action).should().run();
}
@Test
void mockWasInvokedTwice() {
task.execute();
task.execute();
then(action).should(times(2)).run();
}
@Nested
class MockitoBeanFieldInEnclosingClassTests {
@Test
void mockWasInvokedOnce() {
task.execute();
then(action).should().run();
}
@Test
void mockWasInvokedTwice() {
task.execute();
task.execute();
then(action).should(times(2)).run();
}
}
record Task(Runnable action) {
void execute() {
this.action.run();
}
}
@Configuration(proxyBeanMethods = false)
static class TestConfiguration {
@Bean
Task task(Runnable action) {
return new Task(action);
}
}
}