代码之家  ›  专栏  ›  技术社区  ›  katana0815

junitjupiter:如何更改缓存在测试类外部列表中的mockito模拟行为?

  •  0
  • katana0815  · 技术社区  · 5 年前

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

    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 在不同的测试场景中:

    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”和 福 secondTest 以“2不等于3”失败,因为

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

    将改变 ,但有 不 对缓存中模拟对象的影响,该对象将用于调用FooCache#getOrCreateFoo。

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

    0 回复  |  直到 5 年前
        1
  •  2
  •   oliver_t    5 年前

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

    1. 在启动secondTest()之前,会创建另一个新的Foo mock,稍后返回“3”。这将添加到静态缓存中。由于代码是静态的,所以仍然包含第一个mock,这使得assert失败!

    经验教训:

    singleton pattern 是反模式。

    解决:

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

    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
       }
    }
    
        2
  •  1
  •   Yassin Hajaj    5 年前

    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());
    }
    
        3
  •  0
  •   katana0815    5 年前

    只是找到了原因:就像 https://stackoverflow.com/a/16816423/944440 它的第一个和第二个注释描述了:“JUnit设计者希望测试方法之间的测试隔离,所以它创建了一个新的测试类实例来运行每个测试方法。”