Junit Jupiter:如何更改缓存在测试类之外的列表中的模拟模拟行为?



在我的场景中,我有一个简单的类,其中包含一个返回字符串的方法:

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 的模拟对象没有影响。

为什么会这样,在存储在测试类之外的列表中后,我有什么方法可以更改模拟对象的行为?

只是为了解释这里发生的事情:

  1. 在 firstTest() 启动之前,会创建一个新的 Foo 模拟,稍后返回 "2"。这将添加到静态缓存中。断言是正确的。

  2. 在 secondTest() 启动之前,会创建一个新的 Foo 模拟,稍后返回 "3"。这将添加到静态缓存中。由于代码是静态的,因此仍然包含第一个模拟,使断言失败!

要吸取的教训:

静态代码是邪恶的,尤其是静态非常量类属性。甚至工厂也应该以非静态的方式创建/使用。单例模式是一种反模式。

解决 方案:

  1. 从代码中删除所有静态修饰符。

  2. 在每次测试运行时实例化您的 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
}
}

有多种方法可以解决此问题

  1. 重构静态类并包含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();
}
  1. 使用反射访问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();
}
  1. 在每个测试中使用 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的问题,而且在使用单身人士时也是如此

最新更新