Test Plan con i superpoteri: Mocking

Mattia Tommasone

@raibaz

About me

  • 10+ years spent writing (and reading) code
  • FLOSS Enthusiast
  • Former:
    • Developer Advocate @ Google
    • Kids' football coach 
    • University lecturer
    • Conference speaker
  • Current:
    • Lead developer @ Brandon Group
    • Maintainer for MockK

A cosa serve una test suite?

  • Ad avere le idee chiare su cosa deve fare (o non fare) il codice che scriviamo
  • A stanare dei corner case che non erano evidenti
  • A sapere che non stiamo rompendo niente di quello che c'era già
  • Al te stesso di tra sei mesi

Disclaimer

Testing sui libri

class Adder {
    fun add(op1: Int, op2: Int) {
        return op1 + op2
    }
}

Nel mondo reale però...

  • Connessioni a DB
  • Connessioni HTTP
  • Servizi esterni
  • Librerie di terze parti
  • Interazioni non banali tra componenti
  • Side effects
class BusinessLogicService(
    private val persistenceService: MyPersistenceService
) {
    fun doSomethingRelevant(objectId: Long, someValue: Int) {
        val myObject = persistenceService.retrieveObject(objectId)
            ?: throw IllegalArgumentException("$objectId not found")
        
        myObject.someProperty = someValue
        
        persistenceService.saveObject(myObject)
    }
}

perché è difficile da testare?

  • Potresti non avere uno storage a portata di mano
  • La persistenza è al di fuori dello scope di questo componente

esattamente, cosa voglio testare?

  • Le condizioni di errore: cosa succede quando l'oggetto che voglio aggiornare non esiste?
  • L'happy path: se esiste, voglio testare che venga salvato con il valore aggiornato

Serve un mock!

Ma cos'è un mock?

due superpoteri in uno

  • Mi permette di guidare il contesto attorno al mio oggetto in modo che si comporti in un modo predefinito e noto
  • Mi permette di verificare che il mio oggetto si comporti nel modo che mi aspetto rispetto al contesto che gli sta attorno
class BusinessLogicServiceTest {
    @MockK
    private lateinit var persistenceService : MyPersistenceService
    
    private lateinit var service : BusinessLogicService
    
    @Before
    fun setup() {
        MockKAnnotations.init(this)
        
        service = BusinessLogicService(persistenceService)
    }
    
    @Test
    fun `doing something with a non-existing object throws an exception`() {
        every {
            persistenceService.retrieveObject(any())
        } returns null
        
        assertFailsWith<IllegalArgumentException> {
            service.doSomethingRelevant(123L, 345)
        }
    }
}
@Test
fun `doing something with an existing object updates it and saves it`() {
    every {
        persistenceService.retrieveObject(any())
    } returns MyClass()

    service.doSomethingRelevant(123L, 345)
    
    verify {
        persistenceService.saveObject(any())
    }
}
@Test
fun `doing something with an existing object updates it and saves it`() {
    every {
        persistenceService.retrieveObject(any())
    } returns MyClass()

    service.doSomethingRelevant(123L, 345)
    
    val slot = slot<MyClass>()
    verify {
        persistenceService.saveObject(capture(slot))
    }
    
    assertEquals(345, slot.captured.someProperty)
}

perché è così potente?

permette di testare le interazioni (i contratti) tra oggetti

permette di astrarre da entità che potrebbero non esserci durante i test

testare le interazioni può aiutare a far emergere problemi nel design

può essere d'aiuto a fare Cqrs

non è solo per il codice object-oriented

c'è anche per il mio linguaggio?

phake.readthedocs.io

docs.python.org/3/library/unittest.mock.html

https://github.com/mockk/mockk

devcast-codemotion

By Mattia Tommasone

devcast-codemotion

  • 233