Mockito

Testowanie

  |   13 min czytania
(Mockito v2.23)

Dublerzy

Testy jednostkowe rzadko kiedy dotyczą jednostki testowej wolnej od zależności innych obiektów co w konsekwencji prowadzi do definicji problemu tworzenia niezależnych testów na których rezultat nie ma wpływu żadna zależność testowanej jednostki. Wymaganą prawdziwą zależność można czasami dostarczyć ręcznie jednakże jest to kosztowne, obarczone marginesem błędu oraz często niewykonalne (szczególnie w przypadku testów instrumentalnych). Alternatywą dla tworzenia prawdziwych zależności jest dostarczanie dublerów (double test), które można sklasyfikować ze względu na przeznaczenie. Jedna z wielu definicji wyróżnia m.in. następujące obiekty: dummy, fake, stub i mock. Dummy jest przekazywany w celu spełnienia założeń sygnatury jednak jego metody nie są wywoływane. Fake posiada uproszczoną działającą implementacje spełniającą założenia interakcji. Stub dostarcza minimalną implementacją zależności, która pomija proces obliczeniowy i bezpośrednio zwraca zdefiniowany rezultat w taki sposób, aby test wykonał się pozytywnie. Natomiast mock to naiwna implementacja zależności, która bierze udział w procesie testowania i rejestruje nawiązane z nią interakcje, przeznaczona do badania zachowania. Mnogość definicji podziału dublerów oraz wynikające z tego różnice są powodem wielu niejasności, a sama terminologia jest mało istotna w procesie tworzenia zastępników. Pozwala on jednak zrozumieć co dokładnie i w jaki sposób ma zostać przetestowane. Mockito jest frameworkiem wspomagającym pisanie testów jednostkowych poprzez dostarczanie dublerów, które dzieli na zwyczajne Mock oraz częściowe Spy.

Przykład
Na podstawie poniższych klas Manager oraz Data zostaną przedstawione cechy i właściwości zarządzania naiwnymi implementacjami w Mockito.

class Manager(val data: Data) {

    fun fetchData(): String {
        return data.getInfo()
    }

    fun fetchDataWithMessage(message: String): String {
        return data.getInfo(message)
    }
}

data class Data(var number: Int, var text: String) {

    constructor() : this(0, "")

    fun getInfo(): String {
        return "$text $number"
    }

    fun getInfo(message: String?): String {
        return "$message $number"
    }

    fun publicMethod(): String {
        return privateMethod()
    }

    private fun privateMethod(): String {
        return "private returned from public"
    }
}

Tworzenie

Obiekt zastępnika oznaczony adnotacją @Mock lub @Spy może zostać inicjalizowany na kilka sposobów, m.in. poprzez uruchomienie klasy z użyciem @RunWith(MockitoJUnitRunner.class), wykorzystanie zasady MockitoJUnit.rule() lub manualną inicjalizację za pomocą metody MockitoAnnotations.initMocks(). Instancję atrapy można uzyskać statyczną metodą mock() lub spy() co pozwala na pominięcie adnotacji.

//use Mockito runner
@RunWith(MockitoJUnitRunner::class)
class InitMockRunnerTest {

    @Mock
    var obj: Data? = null

    @Test
    fun checkIsObjectInitialized() {
        assertNotNull(obj)
    }
}

//or use Mockito rule
class InitMockRuleTest {

    @Mock
    var obj: Data? = null

    @get:Rule
    var mockitoRule = MockitoJUnit.rule()

    @Test
    fun checkIsObjectInitialized() {
        assertNotNull(obj)
    }
}

//or use manual init method
class InitMockAnnotationTest {

    @Mock
    var obj: Data? = null

    @Test
    fun checkIsObjectInitialized() {
        MockitoAnnotations.initMocks(this)
        assertNotNull(obj)
    }
}

//or use static mock method
class InitMockMethodTest {

    @Test
    fun checkIsObjectInitialized() {
        val obj = mock(Data::class.java)
        assertNotNull(obj)
    }
}

Obiekty dublerów w Mockito mogą zostać stworzone tylko dla typów własnych. Nie można zatem dostarczyć imitacji prymitywów. Co więcej należy przestrzegać zasady, że imitacji podlegają zależności testowanej jednostki, a nie sam testowany obiekt. Adnotacja @InjectMocks automatycznie wstrzykuje imitacje pól oznaczonych jako @Mock lub @Spy do obiektu co jest wykorzystywane jako alternatywa dla konstruktora w sytuacji tworzenia testowanej jednostki zależnej od innych atrap.

@RunWith(MockitoJUnitRunner::class)
class InjectMockTest {

    @Mock
    var mock: Data? = null

    //constructor requires Data arg
    @InjectMocks
    var manager1: Manager? = null

    @Test
    fun checkIsManager1Initialized() {
        assertNotNull(mock)
        assertNotNull(manager1)
    }

    //equivalent
    @Test
    fun checkIsManager2Initialized() {
        var manager2 = Manager(mock)  
        assertNotNull(mock)
        assertNotNull(manager2)
    }
}

Mock i Spy

Mockito wprowadza podział zastępników na zwyczajne (Mock) oraz częściowe (Spy). Mock nie tworzy faktycznej instancji lecz obiekt klasy szkieletowej co pozwala w całości na imitacje wykonania metod klasy bez wpływu na strukturę atrapy. Spy tworzy realną instancje klasy przez co wywołuje faktyczną implementację obiektu dla wszystkich zachowań dla których nie została zdefiniowana naiwna implementacja zachowując przy tym właściwości atrapy (weryfikacja i imitacja).

@RunWith(MockitoJUnitRunner::class)
class MockSpyTest {

    @Mock
    var mock: Data? = null

    @Spy
    var spy: Data? = null

    @Test
    fun checkIsObjectInitialized() {
        assertNotNull(mock)
        assertNotNull(spy)
        mock?.text = "mock"
        spy?.text = "spy"
        assertEquals("mock", mock?.text) //fails mock.text is null
        assertEquals("spy", spy?.text)
    }
}

Konfiguracja

Atrapy mogą być konfigurowane w taki sposób, aby zwracały zadaną wartość w zależności od metody i argumentów. W tym celu należy zdefiniować naiwną implementację m.in. za pomocą instrukcji when.thenReturn czy doReturn.when. Różnica między nimi polega na tym, że when.thenReturn sprawdza typy w trakcie kompilacji, pozwala na zwracanie wielu wartości i w gruncie rzeczy wykonuje wywołaną metodę co w konsekwencji może prowadzić do rzucenia wyjątku. Natomiast doReturn.when wspiera obiekty opakowań (wrapper) oznaczone jako @Spy i metody nie zwracające wartości oraz eliminuje problem wielu argumentów.

@RunWith(MockitoJUnitRunner::class)
class ConfigurationTest {

    @Mock
    lateinit var mock: Data

    @Spy
    lateinit var spy: Data

    lateinit var manager: Manager

    @Before
    fun init() {
        manager = Manager(mock)
    }

    @Test
    fun checkFetchDataReturnsMockValue() {
        assertNull(manager.fetchData())
        `when`(mock.getInfo()).thenReturn("mock")
        assertNull(manager.fetchData()) //now it fails
        assertEquals("mock", manager.fetchData())
    }

    @Test
    fun checkFetchDataReturnsMultipleMockValue() {
        assertNull(manager.fetchData())
        `when`(mock.getInfo()).thenReturn("mock1", "mock2")
        assertNull(manager.fetchData()) //now it fails
        assertEquals("mock1", manager.fetchData())
        assertEquals("mock2", manager.fetchData())
        assertEquals("mock2", manager.fetchData())
    }

    @Test
    fun checkFetchDataTakesMultipleArgReturnsMockValue() {
        manager = Manager(spy)
        doReturn("spy with arg").`when`(spy).getInfo("arg1")
        doReturn("spy with arg").`when`(spy).getInfo("arg2")
        assertEquals("spy with arg", manager.fetchDataWithMessage("arg1"))
        assertEquals("spy with arg", manager.fetchDataWithMessage("arg2"))
        //not possible without reset using when.thenReturn
    }

    @Test
    fun checkGetInfoForWrappedReturnsWrapperValue() {
        val data = Data()
        val spyData = Mockito.spy(data)
        manager = Manager(spyData)
        doReturn("wrapper").`when`(spyData).getInfo()
        assertEquals("wrapper", manager.fetchData())
    }

    @Test
    fun checkFetchDataWithArgsReturnsMockValue() {
        Mockito.`when`(mock.getInfo("arg")).thenReturn("mock with arg")
        assertEquals("mock with arg", manager.fetchDataWithMessage("arg"))
        assertEquals("mock with arg", manager.fetchDataWithMessage("whatever")) //fails
        `when`(mock.getInfo(anyString())).thenReturn("mock with arg")
        assertEquals("mock with arg", manager.fetchDataWithMessage("whatever")) //now it pass
    }

    @Test(expected = IllegalArgumentException::class)
    fun checkFetchDataThrowsExceptionForSpecialCharactersArg() {
        `when`(mock.getInfo("@")).thenThrow(IllegalArgumentException())
        manager.fetchDataWithMessage("@")
    }
}

Dla bardziej złożonych zapytań dla których oczekiwany rezultat nie jest jawny lub zależnych od argumentów można wykorzystać instrukcję thenAnswer lub doAnswer.

@RunWith(MockitoJUnitRunner::class)
class AnswerTest {

    @Test
    fun checkFetchDataWithObjectChangedAndReturnsItsValue() {
        val spy = spy(Data::class.java)
        val manager = Manager(mock)

        assertEquals(" 0", manager.fetchData())
        assertEquals("message 0", manager.fetchDataWithMessage("message"))

        doAnswer(Answer {
            spy.number++
            spy.text = it.arguments[0] as String
            return@Answer spy.getInfo()
        }).`when`(spy).getInfo(anyString())

        assertEquals("message 1", manager.fetchDataWithMessage("message"))
        assertEquals("message 1", manager.fetchData())
    }
}

Weryfikacja

Mockito przechowuje informacje na temat wszystkich wywołań metod wraz z użytymi argumentami. Dzięki temu pozwala na śledzenie działalności atrap oraz weryfikację ich wywołań za pomocą funkcji verify(). Aby uzyskać informacje nt argumentów wywołań funkcji należy wykorzystać ArgumentCaptor.

class VerifyTest {

    lateinit var mock: Data
    lateinit var manager: Manager

    @Before
    fun init() {
        mock = mock(Data::class.java)
        manager = Manager(mock)
    }

    @Test
    fun checkHowManyTimesGetInfoCalled() {
        verifyZeroInteractions(mock) //any methods called on the mock
        verify(mock, never()).getInfo()

        manager.fetchData()

        verify(mock, times(1)).getInfo()
        verify(mock, atLeast(1)).getInfo()
        verify(mock, atMost(3)).getInfo()
        verifyNoMoreInteractions(mock) //no others method called
    }


    @Test
    fun checkArgsOfGetInfo() {
        val captor = forClass(String::class.java)
        manager.fetchDataWithMessage("message")

        //verify args passed into getInfo method
        verify(mock).getInfo(captor.capture())
        assertEquals("message", captor.value)
    }
}

PowerMock

Tworzenie imitacji w Mockito jest obarczone pewnymi ograniczeniami. Niemożliwym jest tworzenie naiwnej implementacji dla statycznych i prywatnych metod klasy czy wywołań konstruktora. Aby temu zaradzić można wykorzystać bibliotekę PowerMock, której zależności pakietów mogą zawierać implementacje JUnit oraz Mockito dzięki czemu zachowana jest zgodność wersji. Klasa testowa korzystająca z PowerMock powinna być uruchamiana z instrukcją @RunWith(PowerMockRunner.class) wraz z adnotacją dla klasy testowanej @PrepareForTest(ClassName.class). PowerMock uzupełnia implementacje Mockito o dodatkowe metody realizacji atrap dla metod statycznych, prywatnych i konstruktorów takie jak np.: mockStatic, verifyPrivate, whenNew.

@RunWith(PowerMockRunner::class)
@PrepareForTest(Data::class, System::class)
class PowerMockTest {

    @Test
    fun checkMockStaticMethod() {
        PowerMockito.mockStatic(System::class.java)
        PowerMockito`when`(System.currentTimeMillis()).thenReturn(100)
        assertEquals(100, System.currentTimeMillis())
    }

    @Test
    fun checkMockPrivateMethod() {
        val obj = Data()
        val spy: Data = PowerMockito.spy(obj)
        assertEquals("private returned from public", spy.publicMethod())
        PowerMockito.doReturn("private mocked").`when`(spy, "privateMethod")
        assertEquals("private mocked", spy.publicMethod())
        PowerMockito.verifyPrivate(spy, times(2)).invoke("privateMethod")
    }

    @Test
    fun checkMockConstructor() {
        val obj = Data()
        assertEquals(0, obj.number)
        assertEquals("", obj.text)
        obj.number = 1
        obj.text = "text"

        PowerMockito.whenNew(Data::class.java).withNoArguments().thenReturn(obj)
        val newObj = Data()
        assertEquals(1, newObj.number)
        assertEquals("text", newObj.text)
    }
}

Jako alternatywę dla Mockito i PowerMock w Kotlin warto rozważyć bibliotekę Mockk, która rozwiązuje problemy występujące w Mockito i wspiera natywne cechy Kotlin takie jak np.: rozszerzenia. Framework Robolectric umożliwi natomiast przeprowadzanie w łatwy sposób lokalnych testów jednostkowych zależnych od Android SDK.