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

Junit Jupiter: How can I change a mockito mock behavior cached in a List outside the test class?

在我的场景中,我有一个简单的 class 方法 returning a String:

public class Foo {
    public String bar() {
        return "1";
    }
}

为了简化List 将存储 Foo 的实例(在现实生活项目中,这是某种 factory/cache组合):

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 的 return 值,我的 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 returns "2" 和 foo 现在存储在缓存列表中。 第二种测试方法 secondTest 失败并显示“2 不等于 3”,因为

when(foo.bar()).thenReturn("3") 

将改变 foo 的行为,但对缓存中将用于调用 FooCache#getOrCreateFoo 的模拟对象 没有 影响。

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

有多种方法可以解决这个问题

  1. 重构静态 class 并包含一个 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());
}

只是为了解释这里发生了什么:

  1. 在 firstTest() 开始之前,创建了一个新的 Foo 模拟,稍后 returns“2”。这被添加到静态缓存中。断言是正确的。

  2. 在启动 secondTest() 之前,创建了另一个新的 Foo 模拟,稍后 returns“3”。这被添加到静态缓存中。由于代码是静态的,第一个 mock 仍然包含,使断言失败!

要学习的课程:

静态代码是邪恶的,尤其是静态非常量 class 属性。 即使是工厂也应该 created/used 以非静态方式。 singleton pattern 是一个反模式。

解决方案:

  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
   }
}

刚刚找到原因:如 及其第一条和第二条评论所述,“JUnit 设计者希望测试方法之间的测试隔离,因此它创建了一个新的测试实例 class 以运行 每个测试方法。"

所以第一个方法的 foo 存储在列表中,随着第二个测试方法的开始,另一个 foo 被创建了!以下任何更改都不会再影响第一个方法 foo。

这不仅是我的 szenario 中的问题,而且在使用 Singletons 时也是如此