在我的场景中,我有一个简单的类,其中包含一个返回字符串的方法:
public class Foo {
public String bar() {
return "1";
}
}
为了简化起见,List
将存储Foo
的实例(在现实生活中的项目中,这是某种工厂/缓存组合):
public class FooCache {
private static List<Foo> cache = new ArrayList<>();
public static Foo getOrCreateFoo(Foo foo) {
if (cache.isEmpty()) {
cache.add(foo);
}
return cache.get(0);
}
}
一旦我尝试在不同的测试场景中重新分配Foo#bar
的返回值,我的 junit 5 测试就会失败:
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class FooTest {
@Mock
private Foo foo;
@Test
void firstTest() {
// Arrange
when(foo.bar()).thenReturn("2");
// Act
Foo uut = FooCache.getOrCreateFoo(foo);
String actual = uut.bar();
// Assert
assertEquals("2", actual);
}
@Test
void secondTest() {
// Arrange
when(foo.bar()).thenReturn("3"); // <--- HAS NO EFFECT ON CACHED FOO
// Act
Foo uut = FooCache.getOrCreateFoo(foo);
String actual = uut.bar();
// Assert
assertEquals("3", actual); // fails with 3 not equals 2
}
}
第一个测试方法firstTest
成功完成,foo
返回"2",foo
现在存储在缓存列表中。 第二种测试方法secondTest
失败并显示"2 不等于 3",因为
when(foo.bar()).thenReturn("3")
将改变foo
的行为,但对缓存中将用于调用 FooCache#getOrCreateFoo 的模拟对象没有影响。
为什么会这样,在存储在测试类之外的列表中后,我有什么方法可以更改模拟对象的行为?
只是为了解释这里发生的事情:
-
在 firstTest() 启动之前,会创建一个新的 Foo 模拟,稍后返回 "2"。这将添加到静态缓存中。断言是正确的。
-
在 secondTest() 启动之前,会创建一个新的 Foo 模拟,稍后返回 "3"。这将添加到静态缓存中。由于代码是静态的,因此仍然包含第一个模拟,使断言失败!
要吸取的教训:
静态代码是邪恶的,尤其是静态非常量类属性。甚至工厂也应该以非静态的方式创建/使用。单例模式是一种反模式。
解决 方案:
从代码中删除所有静态修饰符。
在每次测试运行时实例化您的 FooCache:
public class FooTest {
@Mock
private Foo foo;
// System Under Test (will be instanciated before every test)
// This is the object that you are actually testing.
private FooCache sut = new FooCache();
@Test
void firstTest() {
// Arrange
when(foo.bar()).thenReturn("2");
// Act
Foo uut = sut.getOrCreateFoo(foo);
String actual = uut.bar();
// Assert
assertEquals("2", actual);
}
@Test
void secondTest() {
// Arrange
when(foo.bar()).thenReturn("3");
// Act
Foo uut = sut.getOrCreateFoo(foo);
String actual = uut.bar();
// Assert
assertEquals("3", actual); // fails with 3 not equals 2
}
}
有多种方法可以解决此问题
- 重构静态类并包含
clearCache
方法
public class FooCache {
private static List<Foo> cache = new ArrayList<>();
public static Foo getOrCreateFoo(Foo foo) {
if (cache.isEmpty()) {
cache.add(foo);
}
return cache.get(0);
}
public static void clearFooCache() {
cache.clear();
}
}
在你的测试中
@BeforeEach
public void setUp() {
FooCache.clearCache();
}
- 使用反射访问
FooCache#cache
@BeforeEach
public void setUp() {
Field cache = FooCache.class.getDeclaredField("cache");
cache.setAccessible(true);
List<Foo> listOfFoos = (List<Foo>)cache.get(FooCache.class);
listOfFoos.clear();
}
- 在每个测试中使用 Mockito 的
mockStatic
实用程序
try(MockedStatic<FooCache> theMock = Mockito.mockStatic(YearMonth.class, Mockito.CALLS_REAL_METHODS)) {
doReturn(anyValue).when(theMock).getOrCreateFoo(any());
}
刚刚找到原因:正如 https://stackoverflow.com/a/16816423/944440 及其第一条和第二条评论所描述的那样,"JUnit 设计人员希望在测试方法之间进行测试隔离,因此它创建了一个测试类的新实例来运行每个测试方法。
因此,来自第一种方法的 foo 存储在列表中,并且随着第二个测试方法的开始,已经创建了另一个 foo!以下任何更改都不会再影响第一个方法 foo。
这不仅是我的szenario的问题,而且在使用单身人士时也是如此