Test doubles: dummies, stubs, mocks, fakes

While testing we tend to replace some of the unit’s collaborators with mocks as it is accustomed to call them. The problem with that name is that it is not accurate. The real name of those mocks is test doubles and there are four of them with mock being one of the types.

One reason for this misnaming is the wide usage of mocking frameworks that do not separate the types between them (I am looking at you mockito).

So, lets try to define the four types and see when it is best to use them. We will be using a made up browser and its history and will not use any framework. Just theory:

interface History {
fun push(url: URL)
fun pop(): URL
fun peek(): URL
}
class Browser(
private val history: History
) {
var activeURL: URL? = null
private set
fun visit(url: URL) {
activeURL = if (url == URL("http://default"))
history.peek() else
url
history.push(activeURL!!)
}
fun back() {
history.pop()
activeURL = history.peek()
}
}

Dummies

A dummy is the test double that we use whenever we know that the collaborator will not be used:

@Test fun `a newly created browser does not have an active URL`() {
val browser = Browser(dummyHistory)
assertThat(browser.activeURL, absent())
}
@Test fun `a visited URL is an active URL`() {
val browser = Browser(dummyHistory)
browser.visit(URL("https://www.le0nidas.gr"))
assertThat(browser.activeURL, equalTo(URL("https://www.le0nidas.gr")))
}
private val dummyHistory = object : History {
override fun push(url: URL) {
}
override fun pop(): URL {
TODO("Not yet implemented")
}
override fun peek(): URL {
TODO("Not yet implemented")
}
}

For example in the tests above we just need to check the browser’s active URL. We know that this does not evolve the browser’s history so we pass a collaborator that does nothing on every method call.

Stubs

A stub is the test double that we use whenever the collaborator is being used to query values:

@Test fun `if the visited URL is the default then redirect to the last visited from the browser's history`() {
val browser = Browser(StubHistory(lastVisited = URL("https://www.le0nidas.gr")))
browser.visit(URL("http://default"))
assertThat(browser.activeURL, equalTo(URL("https://www.le0nidas.gr")))
}
private class StubHistory(
private val lastVisited: URL
) : History {
override fun push(url: URL) {
}
override fun pop(): URL {
TODO("Not yet implemented")
}
override fun peek(): URL {
return lastVisited
}
}

For example in the test above we feed the browser with a pre-populated history since we know that the browser will need to peek for the last visited URL.

Mocks

A mock is the test double that we use whenever the collaborator is being used to perform an action:

@Test fun `every visited URL gets saved to the browser's history`() {
val mockHistory = MockHistory()
val browser = Browser(mockHistory)
browser.visit(URL("https://www.le0nidas.gr"))
mockHistory.verifySavedUrlIs(expectedURL = URL("https://www.le0nidas.gr"))
}
private class MockHistory : History {
private var savedURL: URL? = null
override fun push(url: URL) {
savedURL = url
}
override fun pop(): URL {
TODO("Not yet implemented")
}
override fun peek(): URL {
TODO("Not yet implemented")
}
fun verifySavedUrlIs(expectedURL: URL) {
assertThat(savedURL, equalTo(expectedURL))
}
}

For example in the test above we need to make sure that the browser saves the provided URL to its history so we use a collaborator that can verify this behavior.

Fakes

A fake is the test double that we use whenever we need the collaborator to provide us a usable business logic:

@Test fun `going back restores the previously visited URL`() {
val browser = Browser(FakeHistory())
browser.visit(URL("https://www.le0nidas.gr"))
browser.visit(URL("https://www.google.com"))
browser.back()
assertThat(browser.activeURL, equalTo(URL("https://www.le0nidas.gr")))
}
private class FakeHistory : History {
private val urls = mutableListOf<URL>()
override fun push(url: URL) {
urls.add(0, url)
}
override fun pop(): URL {
return urls.removeAt(0)
}
override fun peek(): URL {
return urls[0]
}
}

For example in the test above we need a history instance that works as expected (a simple stack) but without the hassle of having a database or using the file system.

Final thoughts

Having your own test doubles per case makes the code simpler and more readable but does that mean that we should remove our mocking frameworks? In my opinion no. Having a framework saves you a lot of time and keeps things consistent, especially in big projects with lots of developers.

Knowing the theory behind something is always good since it lays a common foundation for discussions and decisions. A mix of the two, framework and theory, could be achieved and help the test code in readability.
For example, we can keep using Mockito’s mock but name the variable stubBlahBlah if is used as a stub. This way the reader will know what to expect.

PS #1: Spock testing framework, besides being a great tool, provides a way to separate stubs from mocks not just in semantics but in usage too (ex: you cannot verify something when using a stub)

PS #2: There is another type of test double called Spy which is a toned down mock that helps in keeping state when a certain behavior takes place but does not verify it.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s