Testing Lessons Learned

The Terrifying Truth of Tortured Tests

The Ideal Test

  • Fast
  • Clear
  • Simple

The Testing Pyramid

contain

Test a Single Thing

enum class FruitType { APPLE, ORANGE}

class Basket {
   private val contents = mutableMapOf<FruitType, Int>()

   fun addApple(amount: Int){
       contents.putIfAbsent(FruitType.APPLE, 0)
       contents[FruitType.APPLE] = getFruitCount(FruitType.APPLE) + amount
   }

   fun addOrange(amount: Int){
       contents.putIfAbsent(FruitType.ORANGE, 0)
       contents[FruitType.ORANGE] = getFruitCount(FruitType.ORANGE) + amount
   }

   fun getFruitCount(type: FruitType): Int {
       return contents[type] ?: 0
   }
}

Test a Single Thing

class TestExamples {

   @Test
   fun addFruit() {
       val basket = Basket()
       basket.addApple(1)
       basket.addOrange(2)

       assertEquals(1, basket.getFruitCount(FruitType.APPLE))
       assertEquals(2, basket.getFruitCount(FruitType.ORANGE))
   }

}

Test a Single Thing

@Test
fun addApple() {
   val basket = Basket()
   basket.addApple(1)

   assertEquals(1, basket.getFruitCount(FruitType.APPLE))
}

@Test
fun addOrange() {
   val basket = Basket()
   basket.addOrange(2)

   assertEquals(2, basket.getFruitCount(FruitType.ORANGE))
}

Test a Single Thing

@Test
fun addApple() {
   val basket = Basket()
   basket.addApple(1)

   assertEquals(1, basket.getFruitCount(FruitType.APPLE))
}

@Test
fun addOrange() {
   val basket = Basket()
   basket.addOrange(2)

   assertEquals(2, basket.getFruitCount(FruitType.ORANGE))
}

Multiple Asserts

data class Person(val name: String, val age: Int) {
   fun disguise(): Person {
       return Person("New Name", age)
   }
}
@Test
fun addFruit() {
   val person = Person("Bob", 10)
   val disguised = person.disguise()

   assertNotEquals(person, disguised)
   assertEquals(person.age, disguised.age)
   assertEquals("New Name", disguised.name)
}

Data Tests with Spock

def "minimum of #a and #b is #c"() {
    expect:
    Math.min(a, b) == c

    where:
    a | b || c
    3 | 7 || 3
    5 | 4 || 4
    9 | 9 || 9
}

Data Tests with KoTest

import io.kotest.core.spec.style.StringSpec
import io.kotest.data.headers
import io.kotest.data.row
import io.kotest.data.table
import io.kotest.matchers.shouldBe

class DistanceConverterTest : StringSpec({
    val converter = DistanceConverter()

    "it should convert meters to kilometers (data.forAll)"{
        io.kotest.data.forAll(
            table(
                headers("meters", "expected kilometers"),
                row(Meter(2000L), Kilometer(2.0)),
                row(Meter(2100L), Kilometer(2.1)),
                row(Meter(3999L), Kilometer(3.99)),
                row(Meter(333L), Kilometer(0.33))
            )
        ) { meters: Meter, kilometers: Kilometer ->
            converter.toKilometer(meters) shouldBe kilometers
        }
    }

})

Test Naming

@Test
fun itDoesTheThing(){

@Test
fun testGetTheFruit(){

@Test
fun getTheFruitTest(){

@Test
fun testGetRedAppleEquals(){

@Test
fun removeAppleSeedsWhenAppleIsSliced(){

Well named test subjects

@Test
fun addApple(){
   val subject = Basket()
   subject.addApple(1)

   assertEquals(1, subject.getFruitCount(FruitType.APPLE))
}

Well named test subjects

@Test
fun addApple(){
   val bskt = Basket()
   bskt.addApple(1)

   assertEquals(1, bskt.getFruitCount(FruitType.APPLE))
}

Assert Tools

@Test
fun assertTools(){
   val a: String? = "thingy"
   val b: String? = null
  
   assertTrue(a.equals(b))
   assertTrue(a != null)
   kotlin.test.assertTrue(a == b)
  
}

At a glance

  • Written for someone else
  • What this specific test is trying to prove
  • What is unique about this test

Extracting Noise

class Past(val events: List<String>)

class YoungPerson(val name: String, val eyeColor: Color, var age: Int, val past: Past){
   fun age(years: Int){
       this.age += years
   }
}

Extracting Noise

@Test
fun noOldPeople(){
   val person = YoungPerson("Bob", Color.blue, 10, Past(listOf("Birthday")))

   assertTrue(person.age < 60)

   assertEquals("Bob", person.name)
   assertEquals(Color.blue, person.eyeColor)
   assertEquals(1, person.past.events.size)
   assertEquals("Birthday", person.past.events.first())
}

@Test
fun personCanAge(){
   val person = YoungPerson("Bob", Color.blue, 11, Past(listOf("Birthday")))
   person.age(1)

   assertEquals(12, person.age)

   assertEquals("Bob", person.name)
   assertEquals(Color.blue, person.eyeColor)
   assertEquals(1, person.past.events.size)
   assertEquals("Birthday", person.past.events.first())
}

Extracting Noise

private fun person(age: Int): YoungPerson{
   return YoungPerson("Bob", Color.blue, age, Past(listOf("Birthday")))
}

private fun assertRealPerson(person: YoungPerson) {
   assertEquals("Bob", person.name)
   assertEquals(Color.blue, person.eyeColor)
   assertEquals(1, person.past.events.size)
   assertEquals("Birthday", person.past.events.first())
}

Extracting Noise

@Test
fun noOldPeople(){
   val person = person(10)

   assertTrue(person.age < 60)
   assertRealPerson(person)
}

@Test
fun personCanAge(){
   val person = person(11)
   person.age(1)

   assertEquals(12, person.age)
   assertRealPerson(person)
}

Test Interface, Not Implementation

  • Manipulating private variables
  • Testing how something is accomplished
  • Revealed in katas
  • Revealed when tests need drastic changes to work with a refactor

Detroit Vs London

  • Integration first or last
  • Mocking vs Injection

The sin of mocking

fun doServiceThing(builder: Builder, transformer: Transformer, polisher: Polisher): Model {
   val model = builder.build()
   val updated = transformer.transform(model)
   return polisher.polish(updated)
}

The sin of mocking

@Test
fun lotsOMocks(){
   val builder = MockBuilder()
   val transformer = MockTransformer()
   val polisher = MockPolisher()

   doServiceThing(builder, transformer, polisher)

   assertTrue(builder.assertCalled())
   assertTrue(transformer.assertCalled())
   assertTrue(polisher.assertCalled())
}

The sin of mocking

  • What did that buy us?
  • What are we testing? (Implementation)
  • Why did those classes need to be mocked? (Side effects? Slowness?)

When to do an integration test

  • Test the glue
  • Not testing the units that make things up
  • Not assuming units will behave a certain way (mocking)