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 klasManager
orazData
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.