Created
July 4, 2023 13:56
-
-
Save ashdavies/bdceea52c95d2cd50f21245b6114acef to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package io.ashdavies.playground | |
import junit.framework.TestCase.assertEquals | |
import org.junit.Assert.assertThrows | |
import org.junit.Before | |
import org.mockito.Mockito | |
import org.mockito.kotlin.any | |
import org.mockito.kotlin.doAnswer | |
import org.mockito.kotlin.doReturn | |
import org.mockito.kotlin.mock | |
import org.mockito.kotlin.spy | |
import org.mockito.kotlin.stub | |
import kotlin.test.Test | |
import kotlin.test.assertFalse | |
import kotlin.test.assertTrue | |
internal class CoffeeMakerTest { | |
/** | |
* It is often recommended to create mocks in the test class, and not in the test method. | |
* This is because creation of mocks is expensive, and shouldn't be repeated unnecessarily. | |
*/ | |
private val mockHeater = mock<Heater>() | |
/** | |
* Many real-world uses configure the mock in the setup, as it may seem prudent to do so. | |
* This however, shares a mutable runtime declaration with each test. | |
* How can a new developer ascertain the correct state of the mock? | |
*/ | |
@Before | |
fun setUp() { | |
mockHeater.stub { | |
onGeneric { heat(any()) } doAnswer { | |
it.getArgument<() -> Any>(0).invoke() | |
} | |
} | |
} | |
/** | |
* Basic test creates instances of concrete classes and asserts the result. Since this example | |
* is simple, it is easy to avoid creating unnecessary test doubles. | |
*/ | |
@Test | |
fun `should make coffee with electric heater`() { | |
val heater = ElectricHeater() | |
val maker = CoffeeMaker( | |
pump = Thermosiphon(heater), | |
heater = heater | |
) | |
assertTrue(maker.brew()) | |
} | |
/** | |
* Fake heater is a wrapper around the concrete heater which allows us to capture the result | |
* of the heat method. This is useful when we want to assert the result of the heat method. | |
*/ | |
@Test | |
fun `should make coffee with fake heater`() { | |
val heater = FakeHeater(ElectricHeater()) | |
val maker = CoffeeMaker( | |
pump = Thermosiphon(heater), | |
heater = heater | |
) | |
maker.brew() | |
val drinks = heater.drinks | |
assertEquals(drinks.size, 1) | |
assertEquals(drinks[0], true) | |
} | |
/** | |
* In this test a mock heater is created before the test starts with an `Answer<*>` to execute | |
* the lambda passed to the heat method. This is necessary since the mock has no referenced | |
* implementation, and must be configured at runtime. This test fails because `isHeating` is | |
* not implemented, and thus always returns false. | |
* | |
* Note that this test uses the existing mockHeater instance, thus taking an already mutable | |
* runtime class definition, that can be difficult to correctly predict. | |
*/ | |
@Test | |
fun `should make coffee with mock heater`() { | |
val maker = CoffeeMaker( | |
pump = Thermosiphon(mockHeater), | |
heater = mockHeater, | |
) | |
assertThrows(AssertionError::class.java) { | |
assertTrue(maker.brew()) | |
} | |
} | |
/** | |
* In this test a mock heater is created before the test starts with an `Answer<*>` to execute | |
* the lambda passed to the heat method. The test then continues to modify the mock as required | |
* further mutating the runtime declaration. | |
*/ | |
@Test | |
fun `should make coffee with modified mock heater`() { | |
val maker = CoffeeMaker( | |
pump = Thermosiphon(mockHeater), | |
heater = mockHeater, | |
) | |
/** | |
* The mock instance is configured after it is passed to another class! | |
* This is a common anti-pattern, and can lead to unexpected behaviour. | |
*/ | |
mockHeater.stub { | |
on { isHeating } doAnswer { true } | |
} | |
assertTrue(maker.brew()) | |
} | |
/** | |
* In this test both a mock heater, and a mock pump are created to execute the test, the mocks | |
* are configured with the appropriate stubbing to make the test pass. | |
*/ | |
@Test | |
fun `should make coffee with mock heater and pump`() { | |
val mockPump = mock<Pump> { | |
on { pump() } doAnswer { true } | |
} | |
val maker = CoffeeMaker( | |
heater = mockHeater, | |
pump = mockPump, | |
) | |
mockHeater.stub { | |
on { isHeating } doAnswer { null } | |
} | |
assertTrue(maker.brew()) | |
} | |
/** | |
* This test demonstrates the use of default answers to mock methods. This is useful when you | |
* may have a nested class that violates the law of demeter, and you want to mock the nested | |
* class without having to create a new mock instance. | |
*/ | |
@Test | |
fun `should mock method with default answers`() { | |
val mockHeater = mock<Heater>(defaultAnswer = Mockito.RETURNS_SMART_NULLS) | |
assertFalse(mockHeater.isHeating) | |
} | |
/** | |
* This test demonstrates how spies can have their actual methods called, which when setting up | |
* mocking behaviour can lead to runtime exceptions. | |
*/ | |
@Test | |
fun `should throw spying real object`() { | |
assertThrows(IndexOutOfBoundsException::class.java) { | |
spy(emptyList<String>()) { | |
on { get(0) } doAnswer { "foo" } | |
} | |
} | |
} | |
@Test | |
fun `should throw stubbing method with vararg`() { | |
val mockDistributor = mock<CoffeeDistributor> { | |
on { announce(any(), any()) } doReturn true | |
} | |
val present = mockDistributor.announce( | |
"Steve", | |
"Roger", | |
"Stan", | |
) | |
assertThrows(AssertionError::class.java) { | |
assertTrue(present) | |
} | |
} | |
} | |
internal class CoffeeService(private val provider: CoffeeStore) { | |
fun get(type: CoffeeType): CoffeeType = when (provider.has(type)) { | |
false -> throw OutOfCoffeeException() | |
true -> type | |
} | |
} | |
internal interface CoffeeDistributor { | |
fun announce(vararg name: String): Boolean | |
} | |
internal interface CoffeeStore { | |
fun has(type: CoffeeType): Boolean | |
} | |
internal enum class CoffeeType { | |
CAPPUCCINO, | |
ESPRESSO, | |
LATTE, | |
} | |
internal interface ComplexContext { | |
fun getBoolean(id: Int): Boolean | |
fun getString(id: Int): String | |
fun getInt(id: Int): Int | |
} | |
internal class OutOfCoffeeException : Exception() | |
/** | |
* The CoffeeMaker class is the class under test. | |
* It is responsible for creating coffee by heating the heater and pumping the pump. | |
* The heater and pump are injected into the CoffeeMaker class, and are thus dependencies. | |
* The heater and pump are interfaces, and thus can be replaced. | |
*/ | |
internal class CoffeeMaker(private val heater: Heater, private val pump: Pump) { | |
fun brew(): Boolean = heater.heat { pump.pump() } | |
} | |
/** | |
* Heater represents a device which can transfer heat while performing an action. | |
* The heat method takes a lambda which performs an action and returns a result. | |
*/ | |
internal interface Heater { | |
fun <T : Any> heat(body: () -> T): T | |
val isHeating: Boolean | |
} | |
/** | |
* Pump represents a device which can pump liquid. The pump method returns a boolean which | |
* represents whether the pump has pumped. | |
*/ | |
internal interface Pump { | |
fun pump(full: Boolean = false): Boolean | |
} | |
/** | |
* Concrete production ready implementation of our Heater interface. | |
*/ | |
internal class ElectricHeater : Heater { | |
override var isHeating: Boolean = false | |
private set | |
override fun <T : Any> heat(body: () -> T): T { | |
isHeating = true | |
val result = body() | |
isHeating = false | |
return result | |
} | |
} | |
/** | |
* Fake implementation of our Heater interface which delegates to the concrete implementation. | |
*/ | |
private class FakeHeater(private val delegate: Heater) : Heater by delegate { | |
private val _drinks = mutableListOf<Any>() | |
val drinks: List<Any> by ::_drinks | |
override fun <T : Any> heat(body: () -> T): T { | |
return delegate.heat(body).also { _drinks += it } | |
} | |
} | |
/** | |
* Concrete implementation of our Pump interface. | |
*/ | |
internal class Thermosiphon(private val heater: Heater) : Pump { | |
override fun pump(full: Boolean) = heater.isHeating | |
} | |
public class FakePump(private val onPump: (Boolean) -> Boolean) : Pump { | |
public val pumped = mutableListOf<Pair<Boolean, Boolean>>() | |
override fun pump(full: Boolean): Boolean = onPump(full).also { | |
pumped += full to it | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment