Skip to content

Mocking and verifying

aramizu edited this page Aug 7, 2019 · 25 revisions

Testing Style Guide

Table of Contents

Introduction

Nomenclatura

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`()

Mockito 101

Mock

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

Spy

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

Style Guide View Model Test

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 o Strictness.STRICT_STUBS para detectar stubs 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 o onChanged é 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))
        }
    }

}

Handling Third-Party Code and Legacy Code

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:

Clone this wiki locally