TDD Mocks Need Love Too
“Make it work. Make it right. Make it fast.”
When using test driven development, sometimes you just need to mock something. Such was the case recently while playing with a weather API. It’s fairly early on in development at this point with really just an entity based on a JSON response from the server that looks something like this.
It occurred to me that sooner or later a view class from the object might be in order. And, of course, we want to test that class, right? But we really don’t want to go to all the trouble of doing an actual call to the server in our test. Tests are supposed to be fast. So I stuffed a json response into a text file to use every time we create the mock.
If we’re using a mock in testing, we expect the values to remain constant. We sure don’t want some test to fail because we changed a value in the mock. So how do we prevent that? We test the mock. And every time we run test we run them all. If a value in the mock changes we’ll catch it.
And without further ado, here’s the completed test suite:
(Using JUnit5 with some fancy stuff to get the log output)
class CurrentWeatherMockTest : UnitTest() {
lateinit var actual: CurrentWeather
@BeforeAll
override fun beforeAll() {
actual = Gson().fromJson(CurrentWeatherMock.json, CurrentWeather::class.java)
println(Values.suiteTitle(Values.SUITE_TITLE_WEATHER_MOCK_CURRENT_WEATHER))
}
@Nested
@DisplayName("API")
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Api {
@BeforeAll
fun setup() {
println(Values.classTitle("API"))
}
@Order(1)
@Test fun `expected methods`() {
print(Values.testTitle("expected methods"))
actualMethods() shouldEqual expectedMethods()
showPassed()
}
}
@Nested
@DisplayName("Behavior")
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class Behavior {
@BeforeAll
fun setup() {
println(Values.classTitle("Behavior"))
}
@Test
@Order(1)
fun `coord values are correct`() {
print(Values.testTitle("coord values are correct"))
actual.coord?.lon shouldEqual -96.8
actual.coord?.lat shouldEqual 32.78
showPassed()
}
@Test
@Order(2)
fun `weather values are correct`() {
print(Values.testTitle("weather values are correct"))
val weather = actual.weather?.get(0)
weather?.id shouldEqual 800
weather?.main shouldEqual "Clear"
weather?.icon shouldEqual "01n"
showPassed()
}
@Test
@Order(3)
fun `main values are correct`() {
print(Values.testTitle("main values are correct"))
val main = actual.main
main?.temp shouldEqual 269.51
main?.pressure shouldEqual 1026
main?.tempMin shouldEqual 266.48
main?.tempMax shouldEqual 271.48
showPassed()
}
@Test
@Order(4)
fun `wind values are correct`() {
print(Values.testTitle("wind values are correct"))
val wind = actual.wind
wind?.speed shouldEqual 1.5
wind?.deg shouldEqual 140
showPassed()
}
@Test
@Order(5)
fun `clouds values are correct`() {
print(Values.testTitle("clouds values are correct"))
val clouds = actual.clouds
clouds?.all shouldEqual 1
showPassed()
}
@Test
@Order(6)
fun `sys values are correct`() {
print(Values.testTitle("sys values are correct"))
val sys = actual.sys
sys?.type shouldEqual 1
sys?.id shouldEqual 4193
sys?.country shouldEqual "US"
sys?.sunrise shouldEqual 1573649968
sys?.sunset shouldEqual 1573687899
showPassed()
}
@Test
@Order(99)
fun `current weather values are correct`() {
print(Values.testTitle("current weather values are correct"))
actual.base shouldEqual "stations"
actual.visibility shouldEqual 16093
actual.dt shouldEqual 1573647508
actual.timezone shouldEqual -21600
actual.id shouldEqual 4684888
actual.name shouldEqual "Dallas"
actual.cod shouldEqual 200
showPassed()
}
}
@Nested
@DisplayName("Corner Cases")
@TestMethodOrder(MethodOrderer.OrderAnnotation::class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
inner class CornerCases {
@BeforeAll
fun setup() {
println(Values.classTitle("Corner Cases"))
}
@Test
@Order(1)
fun `no tests`() {
noTests()
}
}
override fun actualMethods(): List<String> {
return MethodSpy.publicMethodNames(CurrentWeatherMock::class.java)
}
override fun expectedMethods(): List<String> {
return emptyList()
}
}
And here’s the log output:
Current Weather Mock Test Suite 2019-11-14 10:06 AM
API
expected methods => PASSED
Behavior
coord values are correct => PASSED
weather values are correct => PASSED
main values are correct => PASSED
wind values are correct => PASSED
clouds values are correct => PASSED
sys values are correct => PASSED
current weather values are correct => PASSED
Corner Cases
No Tests
End of Test Suite
Passed 8 of 8 tests
Why did I go to all this trouble? I have no idea really. Something to do I suppose. Maybe it’s useful. Maybe it’s not. I’ll leave that up to you to decide.