Bloco

View Original

How to test Android Apps: Unit Testing

Now we dive into unit testing.

The goal is to test a specific part of your code, just a particular class or method. That way, we can find sooner where bugs appear in your application. For that, we need to test that code in isolation, mocking all its dependencies with fake ones we can control.

The most popular library to achieve that is Mockito.

Example

Let's test this class:

class GetAndSetCounter
@Inject constructor(
    private val repository: CounterRepository
) {

    fun get() = repository.getValue()

    fun editCounter(counter: Counter) : Result<Counter> {
        if (counter.value < 0) {
            Timber.w("Value given was ${counter.value}")
            return Result.failure(IncrementError())
        }
        repository.setValue(counter)
        return Result.success(counter)
    }

    class NegativeCounterError :
        Exception("Value given to .editCounter() is negative. Negative numbers are not allowed")
}

Like we explained before, all dependencies are provided in the construtor, to they can easily be changed in testing.

Here's how we can test the editCounter() method:

class GetAndSetCounterTest {

    val mockRepo = mock<CounterRepository>()
    val getAndSetCounter = GetAndSetCounter(mockRepo)

    @Test
    fun editCounter() {
        val expected = Counter(5)
        val result = getAndSetCounter.editCounter(expected)
        verify(mockRepo).setValue(expected)
        assertTrue(result.isSuccess)
        assertEquals(expected, result.getOrNull())
    }
}

In this example, we verify mockRepo.setValue() is properly called every time that getAndSetCounter.editCounter() is called and that the edit was a success.

Since we mocked the Repository, all methods we call from it will be ignored and return null. If we want then to return a specific value or object, we need to stub them with whenever(...).thenReturn(...), for instance:

@Test
fun get() {
    val expected = Counter(5)
    whenever(mockRepo.getValue()).thenReturn(expected)
    val result = getAndSetCounter.get()
    assertEquals(expected, result.getOrNull())
}

This allow us to test the get method without knowing or editing the value of the repository.

Note: you don't usually need to mock value objects like String, List or database models.

Avoiding Android dependencies

Most Android objects don't allow access to their constructors and dependencies. How can we test them? Directly, you can't, not with unit testing, in isolation. But there are tricks to work around some of the issues:

  • Use an architecture like MVP to move your most important code to plain java classes, like Presenters
  • Add setters just for testing (use the @VisibleForTesting annotation) to switch dependencies to a mocked version
  • Use a mocked version of Android with Roboelectric (advanced)

More resources


How to test Android Apps

  1. Introduction
  2. Architecture
  3. Unit Testing
  4. Instrumentation Testing
  5. Other details