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

Viewed 849

In my scenario, I have a simple class with a method returning a String:

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):

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:

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. Second test method secondTest fails with "2 does not equal 3", since

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.

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?

3 Answers

Just to explain what's happening here:

  1. before firstTest() is started, a new Foo mock is created, that later returns "2". This is added to the static cache. The assert is correct.

  2. before secondTest() is started, a another new Foo mock is created, that later returns "3". This is added to the static cache. As the code is static, the first mock is still contained, making the assert to fail!

Lessons to learn:

Static code is evil, especially static non-constant class attributes. Even factories should be created/used in a non-static manner. The singleton pattern is an anti-pattern.

Solutions:

  1. Remove all static modifiers from your code.

  2. Instanciate your FooCache on every test run:

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
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
@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
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."

So the foo from the first method is stored in the list, and with start of second test method, another foo has been created! Any following changes will not affect the first methods foo anymore.

Not only this is a problem in my szenario, but also when working with Singletons

Related