-
Notifications
You must be signed in to change notification settings - Fork 202
Mocking and verifying
- Introduction
- Nomenclatura
- Mockito 101
- Style Guide View Model Test
- Handling Third-Party Code and Legacy Code
Na definição do teste, utilizaremos o padrão da descrição separada por backticks
, para tornar a legibilidade mais natural e, também, inclusiva.
Utilizaremos o template: nome_do_método Should o_que_deveria_fazer When sob_qual_situação
@Test
fun `loadExampleList Should return a list of Strings When service return success`()
A mock in mockito is a normal mock in other mocking frameworks (allows you to stub invocations; that is, return specific values out of method calls).
A spy in mockito is a partial mock in other mocking frameworks (part of the object will be mocked and part will use real method invocations).
Todos os arquivos de teste seguirão um template inicial contendo algumas regras básicas e, possivelmente, alguns dos comandos baixo:
-
A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously. You can use this rule for your host side tests that use Architecture Components.
@get:Rule val rule = InstantTaskExecutorRule()
-
RxSchedulerRule
faz com que o Rx execute em um mesmo thread independentemente dos Schedulers definidos no subscribeOn e observeOn.@get:Rule val rxRule: RxSchedulerRule = RxSchedulerRule()
-
mockitoRule
gera algumas regras que são aplicadas pelo Mockito nos testes que serão implementados. Utilizados oStrictness.STRICT_STUBS
para detectarstubs
fazendo com que o teste falhe. Com esta regra o código se torna mais limpo.@get:Rule val mockitoRule = MockitoJUnit.rule().strictness(Strictness.STRICT_STUBS)
-
Os
@Mock
são instâncias criadas para serem utilizadas nos testes e para serem injetadas em suas respectivas dependências anotdas por@InjectMocks
@Mock lateinit var repository: ExampleListRepository @InjectMocks lateinit var viewModel: ExampleViewModel
-
O comando
whenever
gera stub para o método. Utilizado para retornar um mock quando um método específico é chamado.whenever(repository.getExampleList()).thenReturn(Single.just(list))
-
O comando
clearInvocations
é utilizado para "limpar" o estado do observer, que será verificado, até momento. Pois como queremos validar novos estados, desconsideramos seus estados anteriores.clearInvocations(observer)
-
O comando
verify
em conjunto com oonChanged
é utilizado para verificar um observer e validar o seu estado atual que foi alterado (pela chamada dos métodos na viewModel)verify(observer, times(1)).onChanged(ExampleCommand.OnExampleListLoadedSuccess(list))
-
O comando
argThat
é utilizado para verificar detalhadamente se os dados de objeto enviado a um comando se encontra no estado em que foi alterado:// Um item desta lista foi alterado pelo método `onUpdateItem` viewModel.onUpdateItem(1, "4") // É verificado se o item da lista foi realmente atualizado para o valor esperado verify(observer, times(1)).onChanged(argThat<ExampleCommand.OnUpdateListItem> { this.updatedLExampleList[1] == "4" })
-
Todos os testes seguem a seguinte estrutura:
-
Given
: (dado suas premissas) declaração de variáveis,stubs
,mocks
-
Then
: (então) configura os observers, executa os métodos que serão de fato testados da viewModel -
Should
: (deveria ocorrer) valida as assertivas, os estados observados, retornos
-
Abaixo seguem três cenários completos de teste, utilizando todo o conteúdo supracitado:
@RunWith(JUnit4::class)
class StyleGuideTest {
@get:Rule
val rule = InstantTaskExecutorRule()
@get:Rule
val rxRule: RxSchedulerRule = RxSchedulerRule()
@get:Rule
val mockitoRule = MockitoJUnit.rule()
@Mock
lateinit var application: Application
@Mock
lateinit var repository: ExampleListRepository
@InjectMocks
lateinit var viewModel: ExampleViewModel
@Mock
lateinit var observer: Observer<ExampleCommand>
@Test
fun `loadExampleList Should return a list of Strings When service return success`() {
// Given
val list = listOf("1", "2", "3")
whenever(repository.getExampleList()).thenReturn(Single.just(list))
// Then
viewModel.commandLiveData.observeForever(observer)
viewModel.loadExampleList()
// Should
inOrder(observer) {
verify(observer, times(1)).onChanged(ExampleCommand.ShowLoading(true))
verify(observer, times(1)).onChanged(ExampleCommand.OnExampleListLoadedSuccess(list))
verify(observer, times(1)).onChanged(ExampleCommand.ShowLoading(false))
}
}
@Test
fun `onUpdateItem Should return a updated list of Strings When update a specific item`() {
// Given
val list = listOf("1", "2", "3")
whenever(repository.getExampleList()).thenReturn( Single.just(list))
// Then
viewModel.commandLiveData.observeForever(observer)
viewModel.loadExampleList()
// reset observer status
clearInvocations(observer)
viewModel.onUpdateItem(1, "4")
// Should
verify(observer, times(1)).onChanged(argThat<ExampleCommand.OnUpdateListItem> { this.updatedLExampleList[1] == "4" })
}
@Test
fun `loadExampleList Should a show error message of Strings When service return fail`() {
// Given
whenever(repository.getExampleList()).thenReturn(Single.error(Throwable()))
// Then
viewModel.commandLiveData.observeForever(observer)
viewModel.loadExampleList()
// Should
inOrder(observer) {
verify(observer, times(1)).onChanged(ExampleCommand.ShowLoading(true))
verify(observer, times(1)).onChanged(ExampleCommand.OnExampleListLoadedError)
verify(observer, times(1)).onChanged(ExampleCommand.ShowLoading(false))
}
}
}
Mockito can't mock static methods due to limitations imposed by the way it was implemented:
I think the reason may be that mock object libraries typically create mocks by dynamically creating classes at runtime (using cglib). This means they either implement an interface at runtime (that's what EasyMock does if I'm not mistaken), or they inherit from the class to mock (that's what Mockito does if I'm not mistaken). Both approaches do not work for static members, since you can't override them using inheritance.
The only way to mock statics is to modify a class' byte code at runtime, which I suppose is a little more involved than inheritance.
That's my guess at it, for what it's worth...
https://stackoverflow.com/questions/4482315/why-doesnt-mockito-mock-static-methods
Because of that, handling with Third-Party code and Legacy Code can often bring some challenges.
In order to keep the standards in our codebase we have to use some techniques to be able to test our classes that have dependencies on this kind of code.
For example, our PermissionManager was being implemented this way:
object PermissionManager {
@JvmStatic
fun isPermissionGranted(context: Context?, permissionName: String): Boolean {
if (context == null) return false
return ContextCompat.checkSelfPermission(context, permissionName) == PackageManager.PERMISSION_GRANTED
}
}
Whenever one of our ViewModels had a dependency on this method, it was impossible to unit test the function using it, because it calls code present in the Android Framework:
class ExampleViewModel : AndroidViewModel(application) {
fun example() {
if (PermissionManager.isPermissionGranted(getApplication(), perm)) {
// permission granted
} else {
// permission denied
}
}
}
In order to test it we created the PermissionManagerWrapper
interface:
interface PermissionManagerWrapper {
fun isPermissionGranted(context: Context?, permissionName: String): Boolean
}
And our ExampleViewModel
now has a dependency on the interface instead:
class ExampleViewModel(
private val permissionManager: PermissionManagerWrapper
) : AndroidViewModel(application) {
fun example() {
if (permissionManagerWrapper.isPermissionGranted(getApplication(), perm)) {
// permission granted
} else {
// permission denied
}
}
}
The implementation of the interface used by the ViewModel when the app is executing is just a thin layer on top of the PermissionManager
:
class PermissionManagerWrapperImpl : PermissionManagerWrapper {
override fun isPermissionGranted(context: Context?, permissionName: String): Boolean {
return PermissionManager.isPermissionGranted(context, permissionName)
}
}
When we are executing our tests we can just inject a mocked PermissionManagerWrapper
: