Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Mocking mocking libraries

Avatar for Niek Niek
June 18, 2020

Mocking mocking libraries

Mocking libraries are super popular with developers. They're useful to define expectations, stub method calls, guarantee a particular order of returns, and probably a lot more.

Unfortunately it turns out that tests written with such libraries are actually pretty hard to write, let alone maintain.

In this talk we'll be looking at techniques and a particular architecture style that will ramp up your tests and allows you to get rid of your mocks forever.

Avatar for Niek

Niek

June 18, 2020

Other Decks in Programming

Transcript

  1. Mocking, Stubbing and Verifying • Mockito allows you to provide

    ‘mock’ instances of your classes • Mock instances can be stubbed to return certain results • You can verify function calls on those mock instances
  2. Mocking, Stubbing and Verifying val usersRepository = mock<UsersRepository>() val usersResult

    = mock<List<User#>>() whenever(usersResult.get(0)).thenReturn(User()) whenever(usersRepository.users()).thenReturn(usersResult) verify(usersRepository).users()
  3. Mocking, Stubbing and Verifying • Lately a lot of people

    have been advocating stepping away
 from mocking frameworks like Mockito or MockK • Tests are tightly coupled to implementation details • Tests become fragile and need to be maintained https://ryanharter.com/blog/2020/06/replacing-mocks/
  4. Test Doubles • “Test doubles” is the generic term •

    Dummy objects • Fake objects • Stubs • Spies • Mocks https://martinfowler.com/bliki/TestDouble.html
  5. Test Doubles • “Test doubles” is the generic term •

    Dummy objects • Fake objects • Stubs • Spies • Mocks https://martinfowler.com/bliki/TestDouble.html Dummy objects are passed around but
 never used
  6. Test Doubles • “Test doubles” is the generic term •

    Dummy objects • Fake objects • Stubs • Spies • Mocks https://martinfowler.com/bliki/TestDouble.html Fake objects are working
 implementations, but usually take 
 some shortcut
  7. Test Doubles • “Test doubles” is the generic term •

    Dummy objects • Fake objects • Stubs • Spies • Mocks https://martinfowler.com/bliki/TestDouble.html Stubs provide default answers to calls
 made during test
  8. Test Doubles • “Test doubles” is the generic term •

    Dummy objects • Fake objects • Stubs • Spies • Mocks https://martinfowler.com/bliki/TestDouble.html Spies are like stubs, but also record 
 invocations on the instance
  9. Test Doubles • “Test doubles” is the generic term •

    Dummy objects • Fake objects • Stubs • Spies • Mocks https://martinfowler.com/bliki/TestDouble.html Mocks are pre-programmed with
 expectations which form a
 specification of the calls they are
 expected to receive.
  10. Test Doubles • “Test doubles” is the generic term •

    Dummy objects • Fake objects • Stubs • Spies • Mocks https://martinfowler.com/bliki/TestDouble.html
  11. Fake Objects and Stubs • Simple interfaces can 
 often

    be implemented 
 really easily interface UsersRepository { suspend fun getUsers(): List<User> }
  12. Fake Objects and Stubs • Simple interfaces can 
 often

    be implemented 
 really easily • as a stub interface UsersRepository { suspend fun getUsers(): List<User> } class StubUsersRepository : UsersRepository { override suspend fun getUsers(): List<User> { return emptyList() } }
  13. Fake Objects and Stubs • Simple interfaces can 
 often

    be implemented 
 really easily • as a stub interface UsersRepository { suspend fun getUsers(): List<User> } class StubUsersRepository : UsersRepository { override suspend fun getUsers(): List<User> { return emptyList() } }
  14. Fake Objects and Stubs • Simple interfaces can 
 often

    be implemented 
 really easily • As a stub • Or as a fake interface UsersRepository { suspend fun getUsers(): List<User> } class FakeUsersRepository : UsersRepository { var users: List<User> = emptyList() override suspend fun getUsers(): List<User> { return users } }
  15. Fake Objects and Stubs • Simple interfaces can 
 often

    be implemented 
 really easily • As a stub • Or as a fake interface UsersRepository { suspend fun getUsers(): List<User> } class FakeUsersRepository : UsersRepository { var users: List<User> = emptyList() override suspend fun getUsers(): List<User> { return users } }
  16. Fake Objects and Stubs • Fakes become less
 inviting when

    interfaces
 grow large interface UsersRepository { suspend fun getUsers(): List<User> suspend fun findUser(userId: Long): User? suspend fun saveUser(user: User) suspend fun deleteUser(userId: Long) suspend fun updateName( userId: Long, username: String ) suspend fun updateEmailAddress( userId: Long, emailAddress: String
  17. Fake Objects and Stubs • Fakes become less
 inviting when

    interfaces
 grow large • Especially when using
 a multi-module setup interface UsersRepository { suspend fun getUsers(): List<User> suspend fun findUser(userId: Long): User? suspend fun saveUser(user: User) suspend fun deleteUser(userId: Long) suspend fun updateName( userId: Long, username: String ) suspend fun updateEmailAddress( userId: Long, emailAddress: String
  18. Fake Objects and Stubs • Fakes become less
 inviting when

    interfaces
 grow large • Especially when using
 a multi-module setup class FakeUsersRepository: UsersRepository { var users = emptyList<User>() override suspend fun getUsers(): List<User> { return users } override suspend fun findUser(userId: Long): U TODO("Not yet implemented") } override suspend fun saveUser(user: User) { error("Not used") }
  19. Fake Objects and Stubs • Fakes become less
 inviting when

    interfaces
 grow large • Especially when using
 a multi-module setup • Or when adding a new
 function class FakeUsersRepository: UsersRepository { var users = emptyList<User>() override suspend fun getUsers(): List<User> { return users } override suspend fun findUser(userId: Long): U TODO("Not yet implemented") } override suspend fun saveUser(user: User) { error("Not used") }
  20. Ports & Adapters “Hexagonal Architecture” • Decouples the application’s core

    logic from the services it uses • The core logic consists of the essential algorithms for the application • The services can be replaced without changing the purposes of the application http://www.dossier-andreas.net/software_architecture/ports_and_adapters.html
  21. Ports & Adapters http://www.dossier-andreas.net/software_architecture/ports_and_adapters.html “Hexagonal Architecture” • Primary ports are

    the
 main API • Secondary ports are 
 called by the core logic • Primary adapters form
 the bridge between
 the user and the core 
 logic
  22. Ports & Adapters http://www.dossier-andreas.net/software_architecture/ports_and_adapters.html “Hexagonal Architecture” • Primary ports are

    the
 main API • Secondary ports are 
 called by the core logic • Primary adapters form
 the bridge between
 the user and the core 
 logic • Secondary adapters are
 implementations of the
 secondary ports
  23. Ports & Adapters How does it work? • An instance

    of the application is created, as well as the adapters • The secondary adapters are passed to the core logic (dependency injection) • Primary adapters process user input and pass it to the core logic • Core logic interacts with secondary adapters only (database, network) • Output of the core logic is returned to the user via the primary adapters. http://www.dossier-andreas.net/software_architecture/ports_and_adapters.html
  24. Ports & Adapters When should you use it? http://www.dossier-andreas.net/software_architecture/ports_and_adapters.html •

    If 100% unit test coverage is important to your application • If you want to be able to switch your storage mechanism or any other
 type of third party code • Especially useful for long-lasting applications that need to keep up
 with changing requirements
  25. Testing When using Ports & Adapters • Your core logic

    stands central • Start with a ‘clean slate’ • Your SUT defines the services (secondary ports) it requires • The SUT owns these interfaces
  26. Testing When using Ports & Adapters • Your test suite

    can now implement these services as fakes or stubs • Secondary port interfaces are small by design: • Not subject to change by another component (single responsibility) • Restricted to only the functionality the SUT needs (interface segregation)
  27. Example private fun merge( localWorkouts: List<WorkoutInfo>, remoteWorkoutsTry: Try<List<WorkoutInfo#>> ): List<WorkoutInfo>

    { if (remoteWorkoutsTry !is Try.Success) { return localWorkouts } val filteredRemoteWorkouts = remoteWorkoutsTry.value .filter { remoteWorkout "-> remoteWorkout.id !in localWorkouts.ids() } return localWorkouts .plus(filteredRemoteWorkouts) .sortedBy { it.date } }
  28. Example private fun merge( localWorkouts: List<WorkoutInfo>, remoteWorkoutsTry: Try<List<WorkoutInfo#>> ): List<WorkoutInfo>

    { if (remoteWorkoutsTry !is Try.Success) { return localWorkouts } val filteredRemoteWorkouts = remoteWorkoutsTry.value .filter { remoteWorkout "-> remoteWorkout.id !in localWorkouts.ids() } return localWorkouts .plus(filteredRemoteWorkouts) .sortedBy { it.date } }
  29. Example private fun merge( localWorkouts: List<WorkoutInfo>, remoteWorkoutsTry: Try<List<WorkoutInfo#>> ): List<WorkoutInfo>

    { if (remoteWorkoutsTry !is Try.Success) { return localWorkouts } val filteredRemoteWorkouts = remoteWorkoutsTry.value .filter { remoteWorkout "-> remoteWorkout.id !in localWorkouts.ids() } return localWorkouts .plus(filteredRemoteWorkouts) .sortedBy { it.date } }
  30. Example class WorkoutsProvider( ) { suspend fun workouts(): List<WorkoutInfo> {

    val localWorkouts = #?.localWorkouts() val remoteWorkoutsTry = #?.remoteWorkouts() return merge(localWorkouts, remoteWorkoutsTry) } private fun merge( localWorkouts: List<WorkoutInfo>, remoteWorkoutsTry: Try<List<WorkoutInfo#>> ): List<WorkoutInfo> {#/* #*/} }
  31. Example class WorkoutsProvider( ) { suspend fun workouts(): List<WorkoutInfo> {

    val localWorkouts = #?.localWorkouts() val remoteWorkoutsTry = #?.remoteWorkouts() return merge(localWorkouts, remoteWorkoutsTry) } private fun merge( localWorkouts: List<WorkoutInfo>, remoteWorkoutsTry: Try<List<WorkoutInfo#>> ): List<WorkoutInfo> {#/* #*/} }
  32. Example class WorkoutsProvider( private val localWorkoutsRepository: LocalWorkoutsRepository, private val remoteWorkoutsService:

    RemoteWorkoutsService ) { suspend fun workouts(): List<WorkoutInfo> { val localWorkouts = localWorkoutsRepository.localWorkouts() val remoteWorkoutsTry = remoteWorkoutsService.remoteWorkouts() return merge(localWorkouts, remoteWorkoutsTry) } private fun merge( localWorkouts: List<WorkoutInfo>, remoteWorkoutsTry: Try<List<WorkoutInfo#>> ): List<WorkoutInfo> {#/* #*/} }
  33. Example class WorkoutsProvider( private val localWorkoutsRepository: LocalWorkoutsRepository, private val remoteWorkoutsService:

    RemoteWorkoutsService ) { suspend fun workouts(): List<WorkoutInfo> { val localWorkouts = localWorkoutsRepository.localWorkouts() val remoteWorkoutsTry = remoteWorkoutsService.remoteWorkouts() return merge(localWorkouts, remoteWorkoutsTry) } private fun merge( localWorkouts: List<WorkoutInfo>, remoteWorkoutsTry: Try<List<WorkoutInfo#>> ): List<WorkoutInfo> {#/* #*/} }
  34. Example class WorkoutsProvider( private val localWorkoutsRepository: LocalWorkoutsRepository, private val remoteWorkoutsService:

    RemoteWorkoutsService ) { @SecondaryPort interface LocalWorkoutsRepository { suspend fun localWorkouts(): List<WorkoutInfo> } @SecondaryPort interface RemoteWorkoutsService { suspend fun remoteWorkouts(): Try<List<WorkoutInfo#>> }
  35. Example class FakeLocalWorkoutsRepository : LocalWorkoutsRepository { var workouts = emptyList<WorkoutInfo>()

    override suspend fun localWorkouts(): List<WorkoutInfo> { return workouts } } This implementation will only change if the WorkoutsProvider class changes!
  36. Example class FakeRemoteWorkoutsService : RemoteWorkoutsService { var result: Try<List<WorkoutInfo#>>? =

    null override suspend fun remoteWorkouts(): Try<List<WorkoutInfo#>> { return result#!! } }
  37. Example internal class WorkoutsProviderTest { private val date1 = ZonedDateTime.parse("2020-07-09T10:00:00Z")

    private val workoutInfo1 = WorkoutInfo(id = 1, date = date1) private val localRepository = FakeLocalWorkoutsRepository() private val remoteService = FakeRemoteWorkoutsService() private val provider = WorkoutsProvider( localRepository, remoteService )
  38. Example internal class WorkoutsProviderTest { private val date1 = ZonedDateTime.parse("2020-07-09T10:00:00Z")

    private val workoutInfo1 = WorkoutInfo(id = 1, date = date1) private val localRepository = FakeLocalWorkoutsRepository() private val remoteService = FakeRemoteWorkoutsService() private val provider = WorkoutsProvider( localRepository, remoteService )
  39. Example @Test fun `local workouts are returned`() = runBlocking {

    !/* Given !*/ localRepository.workouts = listOf(workoutInfo1) remoteService.result = Try.Failure(Exception()) !/* When !*/ val result = provider.workouts() !/* Then !*/ expect(result).toContain(workoutInfo1) } }
  40. Example @Test fun `local workouts are returned`() = runBlocking {

    !/* Given !*/ localRepository.workouts = listOf(workoutInfo1) remoteService.result = Try.Failure(Exception()) !/* When !*/ val result = provider.workouts() !/* Then !*/ expect(result).toContain(workoutInfo1) } }
  41. Example @Test fun `local workouts are returned`() = runBlocking {

    !/* Given !*/ localRepository.workouts = listOf(workoutInfo1) remoteService.result = Try.Failure(Exception()) !/* When !*/ val result = provider.workouts() !/* Then !*/ expect(result).toContain(workoutInfo1) } }
  42. Example @Test fun `local workouts are returned`() = runBlocking {

    !/* Given !*/ localRepository.workouts = listOf(workoutInfo1) remoteService.result = Try.Failure(Exception()) !/* When !*/ val result = provider.workouts() !/* Then !*/ expect(result).toContain(workoutInfo1) } }
  43. Example @Test fun `local workouts are returned`() = runBlocking {

    !/* Given !*/ localRepository.workouts = listOf(workoutInfo1) remoteService.result = Try.Failure(Exception()) !/* When !*/ val result = provider.workouts() !/* Then !*/ expect(result).toContain(workoutInfo1) } }
  44. Example @Test fun `local workouts are returned`() = runBlocking {

    !/* Given !*/ whenever(localRepository.workouts).thenReturn(listOf(workoutInfo1)) whenever(remoteService.result).thenReturn(Try.Failure(Exception())) !/* When !*/ val result = provider.workouts() !/* Then !*/ expect(result).toContain(workoutInfo1) } }
  45. Example @Test fun `local workouts are returned`() = runBlocking {

    !/* Given !*/ localRepository.workouts = listOf(workoutInfo1) remoteService.result = Try.Failure(Exception()) !/* When !*/ val result = provider.workouts() !/* Then !*/ expect(result).toContain(workoutInfo1) } }
  46. When can you use Mockito/MockK? • To provide a Dummy

    Object, 
 if you really need to private val provider = WorkoutsProvider( localRepository, mock() #// Not used )
  47. When can you use Mockito/MockK? • To provide a Dummy

    Object, 
 if you really need to • For event callback invocations interface Events { fun onButtonClicked() } val listener = mock<Events>() verify(listener).onButtonClicked()
  48. Wrap up • Fakes allow a ‘more natural’ and stable

    way of providing test doubles • Ports & Adapters help to keep fakes small and concerns separated • Mocking libraries can still be useful
  49. Drawbacks • You’re gonna need more code: more interfaces, more

    adapter classes • It can be difficult sometimes to define the API that defines the boundary
 between your core logic and its external services