UnnecessaryStubbingException w Mockito

Korzystając z Mockito prędzej, czy później spotkamy się z wyjątkiem UnnecessaryStubbingException. Jak nazwa wskazuje, występuje on w momencie, gdy dopuścimy się niepotrzebnego stubbowania, czyli nadamy funkcjonalność mockowi, z której nigdy nie skorzystamy.

Jako że kod mówi więcej niż tysiąc słów, przejdźmy od przykładu. Najpierw prosty serwis, który będziemy mockować:

public class Service {  
  
    public String doSomething(String input) {  
        return input + input;  
    }  
}

Klasa testowa:

@ExtendWith(MockitoExtension.class)  
public class StrictStubbingExample {  
      
    @Mock Service serviceMock;  
  
    @Test  
    public void shouldWorkJustFine() throws Exception {  
        // given  
        Mockito.when(serviceMock.doSomething("a")).thenReturn("1");  
        Mockito.when(serviceMock.doSomething("b")).thenReturn("2"); // nigdy nie używane  
  
        // when        
        String result = serviceMock.doSomething("a");  
  
        // then  
        assertEquals(result, "1");  
    }
}

Uruchomienie spowoduje rzucenie wyjątku o treści:

org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
  1. -> at pl.javastart.junittestingcourse.examples.mockito.strict.StrictStubbingExample.shouldWorkJustFine(StrictStubbingExample.java:28)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.

	at org.mockito.junit.jupiter.MockitoExtension.afterEach(MockitoExtension.java:186)
	(...)

Z wyjątku wynika, że:

  • został on rzucony, ponieważ mamy niepotrzebny kod, a to sprzeczne z ideą kodu, który jest czysty i łatwo się go utrzymuje
  • został on rzucony w metodzie MockitoExtension.afterEach()

Kiedy jest rzucany?

Zanim przejdziemy do możliwych rozwiązań spróbujmy zrozumieć kiedy dokładnie ten wyjątek jest rzucany. Jeśli zmienimy nieco kod i zamiast MockitoExtension skorzystamy z np. MockitoAnnotations.openMocks()

//@ExtendWith(MockitoExtension.class)  
public class StrictStubbingExample {  
  
    @Mock Service serviceMock;  
  
    @Test  
    public void shouldWorkJustFine() throws Exception {  
        // given  
        MockitoAnnotations.openMocks(this);  
        Mockito.when(serviceMock.doSomething("a")).thenReturn("1");  
        Mockito.when(serviceMock.doSomething("b")).thenReturn("2"); // nigdy nie używane  
  
        // when        
        String result = serviceMock.doSomething("a");  
  
        // then  
        assertEquals(result, "1");  
    }
}

Wyjątek nie występuje. Tak samo, gdy utworzymy mocka bez użycia adnotacji

public class StrictStubbingExample {  

    @Test  
    public void shouldWorkJustFine() throws Exception {  
        // given
        Service serviceMock = Mockito.mock(Service.class); // <-- tworzymy mocka "ręcznie"
        Mockito.when(serviceMock.doSomething("a")).thenReturn("1");  
        Mockito.when(serviceMock.doSomething("b")).thenReturn("2"); // nigdy nie używane  
  
        // when        
        String result = serviceMock.doSomething("a");  
  
        // then  
        assertEquals(result, "1");  
    }

Najwidoczniej MockitoExtension ma w sobie jakiś dodatkowy mechanizm. Pogrzebałem trochę i ten test mniej więcej oddaje, co tam się dzieje:

@Test  
public void shouldWorkJustFine() throws Exception {  
    MockitoSession mockitoSession = Mockito.mockitoSession()  
            .startMocking(); // start sesji  
  
    // given    
    Service serviceMock = Mockito.mock(Service.class);  
  
    Mockito.when(serviceMock.doSomething("a")).thenReturn("1");  
    Mockito.when(serviceMock.doSomething("b")).thenReturn("2"); // nigdy nie używane  
  
    // when    
    String result = serviceMock.doSomething("a");  
  
    // then  
    assertEquals(result, "1");  
    mockitoSession.finishMocking(); // zamknięcie sesji i sprawdzenie nieużytych stubów  
}

Przed każdym testem otwierana jest sesja, a po jego zakończeniu jest zamykana. No i właśnie podczas tego zamykania sprawdzane jest, czy każdy stub został wywołany co najmniej raz.

Jak to rozwiązać?

Usunąć niepotrzebne stuby

To zdecydowanie najlepszy pomysł. Trzeba się pozbyć tego, co niepotrzebne i gotowe. W tym przypadku wystarczy usunąć jeden ze stubów:

@Test  
public void shouldWorkJustFine() throws Exception {  
    MockitoSession mockitoSession = Mockito.mockitoSession()  
            .startMocking();
  
    // given    
    Service serviceMock = Mockito.mock(Service.class);  
  
    Mockito.when(serviceMock.doSomething("a")).thenReturn("1");  
    // Mockito.when(serviceMock.doSomething("b")).thenReturn("2"); // nigdy nie używane  
  
    // when    
    String result = serviceMock.doSomething("a");  
  
    // then  
    assertEquals(result, "1");  
    mockitoSession.finishMocking(); 
}

Wydaje się oczywiste, ale w przypadku gdy mamy dodany stub w @BeforeEach i używany we wszystkich testach oprócz jednego, to powyższe rozwiązanie nie jest już takie proste.

Zmienić Strictness

Istnieje możliwość wyłączenia tego mechanizmu poprzez zmianę poziomu Strictness. Możemy to zrobić albo dla całej sesji. Do wyboru mamy:

  • Strictness.LENIENT - wyłącza ten mechanizm
  • Strictness.WARN - drukuje ostrzeżenia w konsoli
  • Strictness.STRICT - domyślny, powoduje rzucenie wyjątku
@Test  
public void shouldWorkJustFine() throws Exception {  
    MockitoSession mockitoSession = Mockito.mockitoSession()  
            .strictness(Strictness.LENIENT) // wyluzuj (domyśnie STRICT_STUBS)  
            .startMocking();  
  
    // given  
    Service serviceMock = Mockito.mock(Service.class);  
  
    Mockito.when(serviceMock.doSomething("a")).thenReturn("1");  
    Mockito.when(serviceMock.doSomething("b")).thenReturn("2"); // nigdy nie używane  
  
    // when    
    String result = serviceMock.doSomething("a");  
  
    // then  
    assertEquals(result, "1");  
    mockitoSession.finishMocking();  
}

Można również dla konkretnego stuba:

@Test  
public void shouldWorkJustFine() throws Exception {  
    MockitoSession mockitoSession = Mockito.mockitoSession()  
            .strictness(Strictness.STRICT_STUBS)  
            .startMocking();  
  
    // given  
    Service serviceMock = Mockito.mock(Service.class);  
  
    Mockito.when(serviceMock.doSomething("a")).thenReturn("1");  
    Mockito.lenient().when(serviceMock.doSomething("b")).thenReturn("2"); // nigdy nie używane  
  
    // when    
    String result = serviceMock.doSomething("a");  
  
    // then  
    assertEquals(result, "1");  
    mockitoSession.finishMocking(); // nie rzuca wyjątku  
}

Zmienić Strictness w MockitoExtension

W przypadku gdy korzystamy z @ExtendWith(MockitoExtension.class) należy dodać adnotację @MockitoSettings i wybrać odpowiedni poziom:

@MockitoSettings(strictness = Strictness.WARN) // domyśnie Strictness.STRICT 
@ExtendWith(MockitoExtension.class)  
public class StrictStubbingExample {  

    @Mock Service serviceMock;  

    @Test  
    public void shouldWorkJustFine() {  
        // given
        Mockito.when(serviceMock.doSomething("a")).thenReturn("1");  
        Mockito.when(serviceMock.doSomething("b")).thenReturn("2"); // nigdy nie używane  
  
        // when        
        String result = serviceMock.doSomething("a");  
  
        // then  
        assertEquals(result, "1");  
    }  
}

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.