简体   繁体   English

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

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

In my scenario, I have a simple class with a method returning a String:在我的场景中,我有一个简单的 class 和一个返回字符串的方法:

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

For simplification , a List will store instances of Foo (in real life project, this is some kind of a factory/cache combination):为了简化,一个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);
    }
}

My junit 5 test will fail as soon as I try to reassign the return value of Foo#bar in different test scenarios:一旦我尝试在不同的测试场景中重新分配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
    }
}

First test method firstTest finishes with success, foo returns "2" and foo is now stored in the cache list.第一个测试方法firstTest成功完成, foo返回“2”并且foo现在存储在缓存列表中。 Second test method secondTest fails with "2 does not equal 3", since第二种测试方法secondTest失败并显示“2 不等于 3”,因为

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

will change the behavior of foo , but has no effect on the mocked object in the cache which will be used calling FooCache#getOrCreateFoo.将改变foo的行为,但对将用于调用 FooCache#getOrCreateFoo 的缓存中的模拟 object没有影响。

Why is that the case and is there any way I can change a mocked object behavior after is got stored in a list outside the test class?为什么会这样,在测试 class 之外的列表中存储后,有什么方法可以更改模拟的 object 行为?

Just to explain what's happening here:只是为了解释这里发生了什么:

  1. before firstTest() is started, a new Foo mock is created, that later returns "2".在开始 firstTest() 之前,创建一个新的 Foo 模拟,稍后返回“2”。 This is added to the static cache.这被添加到 static 缓存中。 The assert is correct.断言是正确的。

  2. before secondTest() is started, a another new Foo mock is created, that later returns "3".在启动 secondTest() 之前,将创建另一个新的 Foo 模拟,稍后返回“3”。 This is added to the static cache.这被添加到 static 缓存中。 As the code is static, the first mock is still contained, making the assert to fail!由于code是static,第一个mock还是包含的,导致assert失败!

Lessons to learn:经验教训:

Static code is evil, especially static non-constant class attributes. Static 代码是邪恶的,尤其是 static 非常量 class 属性。 Even factories should be created/used in a non-static manner.甚至工厂也应该以非静态方式创建/使用。 The singleton pattern is an anti-pattern. singleton 模式是一种反模式。

Solutions:解决方案:

  1. Remove all static modifiers from your code.从代码中删除所有 static 修饰符。

  2. Instanciate your FooCache on every test run:在每次测试运行时实例化您的 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
   }
}

There are multiple ways of solving this issue有多种方法可以解决这个问题

  1. Refactor your static class and include a clearCache method重构您的 static 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();
    }
}

And in your test在你的测试中

@BeforeEach
public void setUp() {
    FooCache.clearCache();
}

  1. Use reflection to access FooCache#cache使用反射访问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. Use Mockito's mockStatic utility inside of each test在每个测试中使用 Mockito 的mockStatic实用程序
try(MockedStatic<FooCache> theMock = Mockito.mockStatic(YearMonth.class, Mockito.CALLS_REAL_METHODS)) {
    doReturn(anyValue).when(theMock).getOrCreateFoo(any());
}

Just found the reason: As in https://stackoverflow.com/a/16816423/944440 and its first and second comments described, "JUnit designers wanted test isolation between test methods, so it creates a new instance of the test class to run each test method."刚刚找到原因:如https://stackoverflow.com/a/16816423/944440及其第一条和第二条评论所述,“JUnit 设计人员希望测试方法之间的测试隔离,因此它创建了一个新的测试实例 class 来运行每种测试方法。”

So the foo from the first method is stored in the list, and with start of second test method, another foo has been created.因此,第一个方法中的 foo 存储在列表中,随着第二个测试方法的开始,另一个 foo 被创建。 Any following changes will not affect the first methods foo anymore.以下任何更改都不会再影响第一个方法 foo。

Not only this is a problem in my szenario, but also when working with Singletons这不仅是我的 szenario 中的问题,而且在使用 Singletons 时也是如此

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM