Superior Testing: Make Fakes not Mocks

February 28, 2019

After years of writing and reading tests I’ve discovered that mocking is either overused or underused. Not sure why exactly it happens but striking the right balance seems to be a complicated issue.

In this Superior Testing article I’ll show how to replace mocking in favor of faking and collect benefits.

Mocking

Sync

Let’s say we have a books repository allowing us to get a book based on its ID. The repository depends on the storage. The resulting code is simple enough.

interface BooksStorage {
    fun getBooks(): List<Book>
}

interface BooksRepository {

    fun getBook(id: String): Book?

    class Impl(storage: BooksStorage) : BooksRepository {

        override fun getBook(id: String) = storage.getBooks().find { it.id == id }
    }
}

Notice that everything is carefully hidden behind interface. This helps a lot with abstraction which leads to better inversion of control practices and utility use-cases (like mocking). Rephrasing a modern classic Beyoncé:

If you like it, then you shoulda put an interface on it.

We’ll make a single test for now — to check that the repository returns null when the storage is blank.

@Test fun testBlankStorage() {
    val storage = mock<BooksStorage> {
        `when`(getBooks()).thenReturn(emptyList())
    }
    val repository = BooksRepository.Impl(storage)

    assertThat(repository.getBook("ID")).isNull()
}

Reactive

Soon enough we notice that book-related operations block the main thread. Also, RxJava is so hot right now (or was, I have no idea). The code evolves.

interface BooksStorage {
    fun getBooks(): Observable<List<Book>>
}

interface BooksRepository {

    fun getBook(id: String): Observable<Optional<Book>>

    class Impl(storage: BooksStorage) : BooksRepository {

        override fun getBook(id: String) = storage.getBooks()
            .map { books -> books.find { it.id == id }.toOptional() }
    }
}

The relevant test needs to be modified as well.

@Test fun testBlankStorage() {
    val storageBooks = PublishSubject.create<List<Book>>()
    val storage = mock<BooksStorage> {
        `when`(getBooks()).thenReturn(storageBooks)
    }
    val repository = BooksRepository.Impl(storage)

    storageBooks.onNext(emptyList())
    repository.getBook("ID").assertValuesOnly(None)
}

Lessons Learned

Good

Meh

Faking

Instead of mocking let’s produce reusable fakes using language instruments and nothing else.

📖 Fakes might be called stubs or dummies — depends on the material. I suggest calling them test implementations.

Sync

The implementation is not complicated at all.

class TestBooksStorage : BooksStorage {
    var getBookResult: String? = null

    override fun getBook(id: String) = getBookResult
}

The relevant test changes, but not by a huge margin.

@Test fun testBlankStorage() {
    val storage = TestBooksStorage().apply {
        getBookResult = null
    }
    val repository = BooksRepository.Impl(storage)

    assertThat(repository.getBook("ID")).isNull()
}

Reactive

Basically the same thing as the sync variant.

class TestBooksStorage : BooksStorage {
    val books = PublishSubject.create<List<Book>>()

    override fun getBooks() = books
}
@Test fun testBlankStorage() {
    val storage = TestBooksStorage()
    val repository = BooksRepository.Impl(storage)

    storage.books.onNext(emptyList())
    repository.getBook("ID").assertValuesOnly(None)
}

There is an interesting note though. If we transform BooksStorage to use val instead of fun we’ll be able to use a more compact notation.

interface BooksStorage {
    val books: Observable<List<Book>>
}

class TestBooksStorage {
    override val books = PublishSubject.create<List<Book>>()
}
@Test fun testBlankStorage() {
    val storage = TestBooksStorage()
    val repository = BooksRepository.Impl(storage)

    storage.books.onNext(emptyList())
    repository.getBook("ID").assertValuesOnly(None)
}

This is possible because in Kotlin referencing an interface implementation gives access to underlying types. In this particular case books is an Observable but we reference it as PublishSubject because we have access to the actual implementation. Use it as an advantage in tests but avoid in inversion of control containers since essentially it might lead to leaking implementation details.

Lessons Learned

Good

Meh

Lessons Learned (Again!)

I think mocking vs. faking is a classic easy vs. simple scenario. It is relatively easy to mock things left and right but is it simple on the scale of the entire codebase? Is it understandable and maintainable? Does it help to make universal and effective tests? It depends on the exact use-case of course. But please, avoid using a microscope as a hammer.