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

Deep Dive into Unit Testing

Jarosław
September 19, 2019

Deep Dive into Unit Testing

Testing our code is essential for development process. Yet, creating clean and readable tests is not a piece of cake. During the talk we'll dive into unit testing concepts and see how Kotlin helps us write efficient test code. We will study examples of testing patterns tailored to common application components. We'll discuss Kotlin testing tools that can bring joy and happiness to everyday work.

Jarosław

September 19, 2019
Tweet

Transcript

  1. TEST STRUCTURE @Test fun `it should return empty string`() {

    //preconditions val useCase = UseCase() //execution val actual = useCase.execute() //assertion val expected = "" assertEquals(expected, actual) } 3
  2. TEST STRUCTURE @Test fun `it should return empty string`() {

    //given val useCase = UseCase() //when val actual = useCase.execute() //then val expected = "" assertEquals(expected, actual) } 4
  3. • Verification of single piece of logic • Isolation •

    Deterministic behavior WHAT MAKES TEST UNIT? 5
  4. TESTS WITH JUNIT4 class DistanceConverterTest { @org.junit.Test fun `it should

    pass`() { // do nothing } @org.junit.Test fun `it should fail`() { throw java.lang.AssertionError(„failed”) } @org.junit.Test fun `it should fail miserably`() { throw RuntimeException() } } 7
  5. JUNIT4 VS JUNIT5 ASSERTIONS class DistanceConverterTest { @Test fun `it

    should fail - junit5`() { org.junit.jupiter.api.Assertions.assertTrue(false) } @Test fun `it should fail - junit4`() { org.junit.Assert.assertTrue(false) } } 9
  6. BOTH ARE THROWING ASSERTION ERRORS: class DistanceConverterTest { @Test fun

    `it should fail - junit5`() { org.junit.jupiter.api.Assertions.assertTrue(false) // under the hood: // org.opentest4j.AssertionFailedError(message, expected, actual, cause) } @Test fun `it should fail - junit4`() { org.junit.Assert.assertTrue(false) // under the hood: // throw new java.lang.AssertionError(message); } } 11
  7. WHAT IF WE HAVE TWO ASSERTIONS IN TEST CASE? class

    DistanceConverterTest { @Test fun `two assertion fails`() { assertTrue(false) assertFalse(true) } @Test fun `first assertion fails, second assertion passes`(){ assertTrue(false) assertFalse(false) } } 12
  8. ASSERTIONS ON EXCEPTIONS 14 class UseCaseThrowingException { fun execute() {

    throw IOException("Terrible thing happened!") } } @Test(expected = IOException::class) fun `it should throw exception`() { val useCase = UseCaseThrowingException() useCase.execute() }
  9. ASSERTIONS ON EXCEPTIONS 15 class UseCaseThrowingException { fun execute() {

    throw IOException("Terrible thing happened!") } } @Test fun `it should throw exception`() { val useCase = UseCaseThrowingException() assertThrows<IOException> { useCase.execute() } }
  10. ASSERTIONS ON EXCEPTIONS 16 class UseCaseThrowingException { fun execute() {

    throw IOException("Terrible thing happened!") } } @Test fun `it should throw exception`() { val useCase = UseCaseThrowingException() assertThrows<IOException>("Terrible thing happened!") { useCase.execute() } }
  11. SIMPLE ASSERTIONS ON EXCEPTIONS @InlineOnly @SinceKotlin("1.3") public inline fun <T,

    R> T.runCatching(block: T.() -> R): Result<R> { return try { Result.success(block()) } catch (e: Throwable) { Result.failure(e) } } 17
  12. SIMPLE ASSERTIONS ON EXCEPTIONS 18 class UseCaseThrowingException { fun execute()

    { throw IOException("Terrible thing happened!") } } @Test fun `it should throw exception`() { val useCase = UseCaseThrowingException() val exception = runCatching { useCase.execute() } .exceptionOrNull() assertTrue(exception is IOException) }
  13. SIMPLE ASSERTIONS ON EXCEPTIONS 19 @Test fun `it should throw

    exception`() { val useCase = UseCaseThrowingException() val exception = runCatching { useCase.execute() } .exceptionOrNull() assertTrue(exception is IOException) } @Test fun `it should not throw any exception`(){ val useCase = UseCaseThrowingException() val exception = runCatching { useCase.execute() } .exceptionOrNull() assertTrue(exception == null) }
  14. class UseCaseTest { @Test fun `it should return empty string`()

    { val useCase = UseCase() val actual = useCase.execute() val expected = "" assertEquals(expected, actual) } } ASSERTIONS 20
  15. class UseCaseTest { @Test fun `it should return empty string`()

    { val useCase = UseCase() val actual = useCase.execute() val expected = "" assertEquals(expected, actual) } } • Does my unit converter produces expected output? ASSERTIONS 20
  16. class UseCaseTest { @Test fun `it should return empty string`()

    { val useCase = UseCase() val actual = useCase.execute() val expected = "" assertEquals(expected, actual) } } • Does my unit converter produces expected output? • Does my Json mapper converts Json to proper objects? ASSERTIONS 20
  17. class UseCaseTest { @Test fun `it should return empty string`()

    { val useCase = UseCase() val actual = useCase.execute() val expected = "" expectThat(expected).isEqualTo(actual) } } • Built in methods in junit4 and junit5 • Built in methods in KotlinTest • Truth • Strikt ASSERTION TOOLS 21
  18. PARAMETERIZED TESTS • Does my unit converter produces expected output

    for various arguments? typealias Meter = Long typealias Kilometer = Double class DistanceConverter { fun parse(distance: Meter): Kilometer { return when { distance in 0..999 -> distance.div(1000.0) distance in 1000..10_000 -> distance.div(1000.0) distance > 10_000 -> distance.div(1000.0) else -> throw UnsupportedOperationException() } } } 23
  19. @RunWith(Parameterized::class) class DistanceConverterTest(val input: Input){ companion object { @JvmStatic @Parameterized.Parameters

    fun data(): Collection<Input> { return listOf( Input(500, 0.5), Input(750, 0.8) ) } } @Test fun `check distance parser`(){ val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected, 0.0) } data class Input(val parameter: Meter, val expected: Kilometer) } 24
  20. @RunWith(Parameterized::class) class DistanceConverterTest(val input: Input){ companion object { @JvmStatic @Parameterized.Parameters

    fun data(): Collection<Input> { return listOf( Input(500, 0.5), Input(750, 0.8) ) } } @Test fun `check distance parser`(){ val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected, 0.0) } data class Input(val parameter: Meter, val expected: Kilometer) } parameterized runner 24
  21. @RunWith(Parameterized::class) class DistanceConverterTest(val input: Input){ companion object { @JvmStatic @Parameterized.Parameters

    fun data(): Collection<Input> { return listOf( Input(500, 0.5), Input(750, 0.8) ) } } @Test fun `check distance parser`(){ val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected, 0.0) } data class Input(val parameter: Meter, val expected: Kilometer) } parameterized runner 24
  22. @RunWith(Parameterized::class) class DistanceConverterTest(val input: Input){ companion object { @JvmStatic @Parameterized.Parameters

    fun data(): Collection<Input> { return listOf( Input(500, 0.5), Input(750, 0.8) ) } } @Test fun `check distance parser`(){ val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected, 0.0) } data class Input(val parameter: Meter, val expected: Kilometer) } parameterized runner static argument provider 24
  23. @RunWith(Parameterized::class) class DistanceConverterTest(val input: Input){ companion object { @JvmStatic @Parameterized.Parameters

    fun data(): Collection<Input> { return listOf( Input(500, 0.5), Input(750, 0.8) ) } } @Test fun `check distance parser`(){ val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected, 0.0) } data class Input(val parameter: Meter, val expected: Kilometer) } parameterized runner static argument provider 24
  24. 25

  25. 25

  26. class DistanceConverterTest { @ParameterizedTest @ArgumentsSource(TestInputProvider::class) fun `check distance parser`(input: Input)

    { val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected) } class TestInputProvider : ArgumentsProvider { override fun provideArguments( context: ExtensionContext? ): Stream<out Arguments> { return Stream.of( Input(500, 0.5), Input(750, 0.8) ).map { Arguments.of(it) } } } } 26
  27. class DistanceConverterTest { @ParameterizedTest @ArgumentsSource(TestInputProvider::class) fun `check distance parser`(input: Input)

    { val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected) } class TestInputProvider : ArgumentsProvider { override fun provideArguments( context: ExtensionContext? ): Stream<out Arguments> { return Stream.of( Input(500, 0.5), Input(750, 0.8) ).map { Arguments.of(it) } } } } parameterized test method 26
  28. class DistanceConverterTest { @ParameterizedTest @ArgumentsSource(TestInputProvider::class) fun `check distance parser`(input: Input)

    { val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected) } class TestInputProvider : ArgumentsProvider { override fun provideArguments( context: ExtensionContext? ): Stream<out Arguments> { return Stream.of( Input(500, 0.5), Input(750, 0.8) ).map { Arguments.of(it) } } } } parameterized test method 26
  29. class DistanceConverterTest { @ParameterizedTest @ArgumentsSource(TestInputProvider::class) fun `check distance parser`(input: Input)

    { val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected) } class TestInputProvider : ArgumentsProvider { override fun provideArguments( context: ExtensionContext? ): Stream<out Arguments> { return Stream.of( Input(500, 0.5), Input(750, 0.8) ).map { Arguments.of(it) } } } } parameterized test method ArgumentsProvider class 26
  30. class DistanceConverterTest { @ParameterizedTest @ArgumentsSource(TestInputProvider::class) fun `check distance parser`(input: Input)

    { val converter = DistanceConverter() val actual = converter.parse(input.parameter) val expected = input.expected assertEquals(actual, expected) } class TestInputProvider : ArgumentsProvider { override fun provideArguments( context: ExtensionContext? ): Stream<out Arguments> { return Stream.of( Input(500, 0.5), Input(750, 0.8) ).map { Arguments.of(it) } } } } parameterized test method ArgumentsProvider class 26
  31. 27

  32. 27

  33. class DistanceConverterTest : StringSpec({ val converter = DistanceConverter() „Convert meter

    to kilometer" { val inputs = listOf( Input(500L, 0.5), Input(750L, 0.8) ) inputs.forAll { (meter, kilometer) -> meter.toKilometer() shouldBe kilometer } } }) 28
  34. class DistanceConverterTest : StringSpec({ val converter = DistanceConverter() „Convert meter

    to kilometer" { val inputs = listOf( Input(500L, 0.5), Input(750L, 0.8) ) inputs.forAll { (meter, kilometer) -> meter.toKilometer() shouldBe kilometer } } }) lambda with test source 28
  35. class DistanceConverterTest : StringSpec({ val converter = DistanceConverter() „Convert meter

    to kilometer" { val inputs = listOf( Input(500L, 0.5), Input(750L, 0.8) ) inputs.forAll { (meter, kilometer) -> meter.toKilometer() shouldBe kilometer } } }) lambda with test source 28
  36. class DistanceConverterTest : StringSpec({ val converter = DistanceConverter() „Convert meter

    to kilometer" { val inputs = listOf( Input(500L, 0.5), Input(750L, 0.8) ) inputs.forAll { (meter, kilometer) -> meter.toKilometer() shouldBe kilometer } } }) lambda with test source easy collection testing 28
  37. class DistanceConverterTest : StringSpec({ val converter = DistanceConverter() „Convert meter

    to kilometer" { val inputs = listOf( Input(500L, 0.5), Input(750L, 0.8) ) inputs.forAll { (meter, kilometer) -> meter.toKilometer() shouldBe kilometer } } }) lambda with test source easy collection testing 28
  38. 29

  39. 29

  40. 29 java.lang.AssertionError: 1 elements passed but expected 4 The following

    elements passed: Input(meter=500, kilometer=0.5) The following elements failed: Input(meter=750, kilometer=0.8) => expected: 0.8 but was: 0.75 Input(meter=730, kilometer=0.8) => expected: 0.8 but was: 0.73 Input(meter=710, kilometer=0.8) => expected: 0.8 but was: 0.71
  41. class DistanceParserParametrizedTest : Spek({ describe("distance parser") { val converter =

    DistanceConverter() listOf(Input(500, 0.5),Input(750, 0.8)) .forEach { (param, expected) -> it("should parse ${param}m to ${expected}km") { assertEquals(expected, param.toKilometers()) } } } }) 30
  42. class DistanceParserParametrizedTest : Spek({ describe("distance parser") { val converter =

    DistanceConverter() listOf(Input(500, 0.5),Input(750, 0.8)) .forEach { (param, expected) -> it("should parse ${param}m to ${expected}km") { assertEquals(expected, param.toKilometers()) } } } }) lambda with test source 30
  43. class DistanceParserParametrizedTest : Spek({ describe("distance parser") { val converter =

    DistanceConverter() listOf(Input(500, 0.5),Input(750, 0.8)) .forEach { (param, expected) -> it("should parse ${param}m to ${expected}km") { assertEquals(expected, param.toKilometers()) } } } }) lambda with test source 30
  44. class DistanceParserParametrizedTest : Spek({ describe("distance parser") { val converter =

    DistanceConverter() listOf(Input(500, 0.5),Input(750, 0.8)) .forEach { (param, expected) -> it("should parse ${param}m to ${expected}km") { assertEquals(expected, param.toKilometers()) } } } }) lambda with test source easy test grouping and nesting 30
  45. class DistanceParserParametrizedTest : Spek({ describe("distance parser") { val converter =

    DistanceConverter() listOf(Input(500, 0.5),Input(750, 0.8)) .forEach { (param, expected) -> it("should parse ${param}m to ${expected}km") { assertEquals(expected, param.toKilometers()) } } } }) lambda with test source easy test grouping and nesting 30
  46. class DistanceParserParametrizedTest : Spek({ describe("distance parser") { val converter =

    DistanceConverter() listOf(Input(500, 0.5),Input(750, 0.8)) .forEach { (param, expected) -> it("should parse ${param}m to ${expected}km") { assertEquals(expected, param.toKilometers()) } } } }) lambda with test source easy test grouping and nesting test functions are extension functions 30
  47. class DistanceParserParametrizedTest : Spek({ describe("distance parser") { val converter =

    DistanceConverter() listOf(Input(500, 0.5),Input(750, 0.8)) .forEach { (param, expected) -> it("should parse ${param}m to ${expected}km") { assertEquals(expected, param.toKilometers()) } } } }) lambda with test source easy test grouping and nesting test functions are extension functions 30
  48. • Check just enough test cases • Have single test

    cases described well BEST PRACTICES IN PARAMETERIZED TESTS 32
  49. • Check just enough test cases • Have single test

    cases described well • Make use of type safety (Input class) BEST PRACTICES IN PARAMETERIZED TESTS 32
  50. • Check just enough test cases • Have single test

    cases described well • Make use of type safety (Input class) • Don’t repeat yourself - write extensions for common cases BEST PRACTICES IN PARAMETERIZED TESTS 32
  51. • We usually shouldn’t test loggers… • Non-fatal errors logging

    for Crashlytics • Existing solution: custom Timber.Tree TESTING SIDE EFFECTS 33
  52. TESTING SIDE EFFECTS class CrashlyticsTree : Timber.Tree() { override fun

    log(priority: Int, tag: String?, message: String, throwable: Throwable?) { if (priority == Log.VERBOSE || priority == Log.DEBUG) { return } Crashlytics.log(priority, tag, message) if (throwable != null) { Crashlytics.logException(throwable) } } } 34
  53. TESTING SIDE EFFECTS class SideEffects(){ init { logNonFatal("should not be

    in this state") } } @Test fun `on fail it should add log`(){ val logs = mutableListOf<String>() Timber.plant(object : Timber.Tree() { override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { logs.add(message) } }) SideEffects() // assertion on logs } 36
  54. TESTING SIDE EFFECTS fun withLoggingContext(testBlock: (FakeTimberTree) -> Unit) { val

    timberTree = FakeTimberTree() Timber.plant(timberTree) testBlock(timberTree) Timber.uprootAll() } class FakeTimberTree : Timber.Tree() { val listOfLogs = mutableListOf<Log>() override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { listOfLogs.add(Log(priority, tag, message, t)) } } 37
  55. TESTING SIDE EFFECTS @Test fun `on fail it should log

    non-fatal`() { withLoggingContext { val sideEffects = SideEffects() expectThat(it.listOfLogs.first().message) .isEqualTo("should not be in this state") } } 38
  56. TESTING SIDE EFFECTS • Do not introduce side effects! •

    If you have to (legacy code, hacking external library, etc.) give them some nice abstraction 39
  57. • We create test doubles to keep test in isolation

    • They simulate other parts of system TEST DOUBLES 40
  58. • We create test doubles to keep test in isolation

    • They simulate other parts of system • Dependency inversions principle! TEST DOUBLES 40
  59. • We create test doubles to keep test in isolation

    • They simulate other parts of system • Dependency inversions principle! • Mock, Stub, Fake, etc. TEST DOUBLES 40
  60. TEST DOUBLES • Fake - lightweight implementation • Stub -

    returns some predefined value • Mock - to check function invocations 41
  61. TEST DOUBLES • Fake - lightweight implementation • Stub -

    returns some predefined value • Mock - to check function invocations • We often call all test doubles mocks 41
  62. EXAMPLE class VenueViewModel(val dataProvider: DataProvider<String, VenueDto>) : ViewModel() { var

    venues: List<VenueDto> = emptyList() fun start() { dataProvider.getAll().let { venues = it } } } 42
  63. EXAMPLE @Test fun `given non-empty dataset when start then load

    list`() { val givenDtoList = listOf( VenueDto(id = "1", content = "Some data") ) val venueViewModel = VenueViewModel( dataProvider = object : DataProvider<String, VenueDto> { override fun getById(id: String): VenueDto { TODO("not implemented") } override fun getAll(): List<VenueDto> { return givenDtoList } } ) venueViewModel.start() expectThat(venueViewModel.venues).containsExactly(givenDtoList) } 43
  64. EXAMPLE @Test fun `given non-empty dataset when start then load

    list`() { val givenDtoList = listOf( VenueDto(id = "1", content = "Some data") ) val venueViewModel = VenueViewModel( dataProvider = mock { on { getAll() } doReturn givenDtoList } ) venueViewModel.start() expectThat(venueViewModel.venues).containsExactly(givenDtoList) } 44
  65. MOCKITO @RunWith(MockitoJUnitRunner::class) class PresenterTest { @Mock lateinit var view: View

    @Mock lateinit var dataProvider: DataProvider @Test fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) `when`(dataProvider.getAll()).thenReturn(elements) val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } 46
  66. MOCKITO @RunWith(MockitoJUnitRunner::class) class PresenterTest { @Mock lateinit var view: View

    @Mock lateinit var dataProvider: DataProvider @Test fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) `when`(dataProvider.getAll()).thenReturn(elements) val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } @Mock won’t work without it 46
  67. MOCKITO @RunWith(MockitoJUnitRunner::class) class PresenterTest { @Mock lateinit var view: View

    @Mock lateinit var dataProvider: DataProvider @Test fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) `when`(dataProvider.getAll()).thenReturn(elements) val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } @Mock won’t work without it 46
  68. MOCKITO @RunWith(MockitoJUnitRunner::class) class PresenterTest { @Mock lateinit var view: View

    @Mock lateinit var dataProvider: DataProvider @Test fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) `when`(dataProvider.getAll()).thenReturn(elements) val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } @Mock won’t work without it cannot be val 46
  69. MOCKITO @RunWith(MockitoJUnitRunner::class) class PresenterTest { @Mock lateinit var view: View

    @Mock lateinit var dataProvider: DataProvider @Test fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) `when`(dataProvider.getAll()).thenReturn(elements) val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } @Mock won’t work without it cannot be val 46
  70. MOCKITO @RunWith(MockitoJUnitRunner::class) class PresenterTest { @Mock lateinit var view: View

    @Mock lateinit var dataProvider: DataProvider @Test fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) `when`(dataProvider.getAll()).thenReturn(elements) val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } @Mock won’t work without it cannot be val reserved word in Kotlin 46
  71. MOCKITO @RunWith(MockitoJUnitRunner::class) class PresenterTest { @Mock lateinit var view: View

    @Mock lateinit var dataProvider: DataProvider @Test fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) `when`(dataProvider.getAll()).thenReturn(elements) val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } @Mock won’t work without it cannot be val reserved word in Kotlin 46
  72. KOTLIN-MOCKITO class PresenterTest { val view: View = mock() @Test

    fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) val dataProvider: DataProvider = mock { on { getAll() } doReturn elements } val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } 47
  73. KOTLIN-MOCKITO class PresenterTest { val view: View = mock() @Test

    fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) val dataProvider: DataProvider = mock { on { getAll() } doReturn elements } val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } inline, reifeid 47
  74. KOTLIN-MOCKITO class PresenterTest { val view: View = mock() @Test

    fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) val dataProvider: DataProvider = mock { on { getAll() } doReturn elements } val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } inline, reifeid 47
  75. KOTLIN-MOCKITO class PresenterTest { val view: View = mock() @Test

    fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) val dataProvider: DataProvider = mock { on { getAll() } doReturn elements } val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } inline, reifeid clean syntax 47
  76. KOTLIN-MOCKITO class PresenterTest { val view: View = mock() @Test

    fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) val dataProvider: DataProvider = mock { on { getAll() } doReturn elements } val presenter = Presenter(view, dataProvider) presenter.start() verify(view).displayItems(elements) } } inline, reifeid clean syntax 47
  77. MOCKK class PresenterTest { val view: View = mockk(relaxUnitFun =

    true) @Test fun `given non-empty list when presenter start then display elements on view`() { val elements = listOf( Element(1, "first"), Element(2, "second") ) val dataProvider = mockk<DataProvider>() every { dataProvider.getAll() } returns elements val presenter = Presenter(view, dataProvider) presenter.start() verify { view.displayItems(elements) } } } 48
  78. MOCKING BEST PRACTICES • Think twice before mocking final classes

    • Reuse mocks carefully - no one needs shared state 49
  79. MOCKING BEST PRACTICES • Think twice before mocking final classes

    • Reuse mocks carefully - no one needs shared state • Extract common mocks / fakes to separate classes 49
  80. MOCKING BEST PRACTICES • Think twice before mocking final classes

    • Reuse mocks carefully - no one needs shared state • Extract common mocks / fakes to separate classes • Try to implement your own fakes - you don’t have to trust what’s going inside mocking frameworks 49
  81. FACTORY METHODS + DEFAULT ARGUMENTS class SignUpPresenter( val view: SignUpView,

    val router: Router, val formValidator: FormValidator, val userProvider: UserProvider, val signUpUseCase: SignUpUseCase ){ /**/ } 51
  82. FACTORY METHODS + DEFAULT ARGUMENTS fun createPresenter( router: Router =

    mock(), signUpView: SignUpView = mock(), userProvider: UserProvider = mock(), formValidator: FormValidator = mock(), signUpUseCase: SignUpUseCase = mock() ): SignUpPresenter { return SignUpPresenter( signUpView, router, formValidator, userProvider, signUpUseCase ) } 52
  83. FACTORY METHODS + DEFAULT ARGUMENTS @Test fun `given … when

    … then propagate error to view`(){ val useCase: SignUpUseCase = mock { on { execute(any(), any()) } doThrow Exception() } val presenter = createPresenter( signUpUseCase = useCase ) verify(signUpView).showError() } 53
  84. MODEL - VIEW - PRESENTER interface View : BaseView {

    fun addDestination(pub: PubModel) fun addSuggestions(routes: List<RouteElement>) fun updateUserLocations(userLocations: List<UserLocationModel>) } interface Presenter : BasePresenter<View> { fun loadTripSuggestions(from: LatLng, to: PubModel) } 55
  85. MODEL - VIEW - PRESENTER interface View : BaseView {

    fun addDestination(pub: PubModel) fun addSuggestions(routes: List<RouteElement>) fun updateUserLocations(userLocations: List<UserLocationModel>) } interface Presenter : BasePresenter<View> { fun loadTripSuggestions(from: LatLng, to: PubModel) } 55 it("should add suggestions to view") { verify(view).addDestination(givenDestination) }
  86. EXECUTING TEST IN MVP FLOW fun <View> Presenter<View>.withFlowOn( view: View,

    function: View.() -> Unit) { attachView(view) start() function.invoke(view) detachView() } presenter.withFlowOn(view){ it("should add suggestions to view") { verify(view).addDestination(givenDestination) } } 56
  87. VIEWMODEL abstract class ViewModel { protected fun onCleared() {} }

    57 abstract class BaseViewModel : ViewModel() { open fun close() { //clear resources } override fun onCleared() { close() super.onCleared() } }
  88. VIEWMODEL abstract class ViewModel { protected fun onCleared() {} }

    57 abstract class BaseViewModel : ViewModel() { open fun close() { //clear resources } override fun onCleared() { close() super.onCleared() } } invoke close in test flow
  89. VIEWMODEL abstract class ViewModel { protected fun onCleared() {} }

    57 abstract class BaseViewModel : ViewModel() { open fun close() { //clear resources } override fun onCleared() { close() super.onCleared() } } invoke close in test flow
  90. MODEL-VIEW-VIEWMODEL 59 class SignUpViewModel( private val useCase: SignUpUseCase, private val

    router: Router ) : BaseViewModel() { var progressVisible by observable(false) fun signUpClick() { useCase.execute(signUpEntity) .doOnSubscribe { progressVisible = true } .doOnComplete { progressVisible = false } .subscribe({},{}) } fun signInClick() { router.openSignIn() } }
  91. MODEL-VIEW-VIEWMODEL fun <VM : BaseViewModel> VM.withFlow(testBody: (VM) -> Unit) {

    start() testBody.invoke(this) close() } viewModel.withFlow{ signInClick() verify(router).openSignIn() } 60
  92. MODEL- VIEW-INTENT 61 interface View : MvpView { fun initIntent():

    Observable<ConversationId> fun sendMessageIntent(): Observable<Message> fun openProfileIntent(): Observable<ProfileId> fun closeIntent(): Observable<Any> fun render(viewState: ChatViewState) } abstract class Presenter : MviBasePresenter<View, ChatViewState>()
  93. MODEL- VIEW-INTENT 61 interface View : MvpView { fun initIntent():

    Observable<ConversationId> fun sendMessageIntent(): Observable<Message> fun openProfileIntent(): Observable<ProfileId> fun closeIntent(): Observable<Any> fun render(viewState: ChatViewState) } abstract class Presenter : MviBasePresenter<View, ChatViewState>() verify(view).what???()
  94. val renderedStates = mutableListOf<ChatViewState>() val initSubject = PublishSubject.create<ConversationId>() val sendMessageSubject

    = PublishSubject.create<Message>() val openProfileSubject = PublishSubject.create<ProfileId>() val view: ChatContract.View = object : ChatContract.View { override fun initIntent(): Observable<Long> = initSubject override fun sendMessageIntent(): Observable<Message> = sendMessageSubject override fun openProfileIntent(): Observable<ProfileId> = openProfileSubject override fun render(viewState: ChatViewState) { renderedStates.add(viewState) } } init { presenter.attachView(view) } VIEW ROBOT 62
  95. val renderedStates = mutableListOf<ChatViewState>() val initSubject = PublishSubject.create<ConversationId>() val sendMessageSubject

    = PublishSubject.create<Message>() val openProfileSubject = PublishSubject.create<ProfileId>() val view: ChatContract.View = object : ChatContract.View { override fun initIntent(): Observable<Long> = initSubject override fun sendMessageIntent(): Observable<Message> = sendMessageSubject override fun openProfileIntent(): Observable<ProfileId> = openProfileSubject override fun render(viewState: ChatViewState) { renderedStates.add(viewState) } } init { presenter.attachView(view) } VIEW ROBOT subjects to control intents 62
  96. val renderedStates = mutableListOf<ChatViewState>() val initSubject = PublishSubject.create<ConversationId>() val sendMessageSubject

    = PublishSubject.create<Message>() val openProfileSubject = PublishSubject.create<ProfileId>() val view: ChatContract.View = object : ChatContract.View { override fun initIntent(): Observable<Long> = initSubject override fun sendMessageIntent(): Observable<Message> = sendMessageSubject override fun openProfileIntent(): Observable<ProfileId> = openProfileSubject override fun render(viewState: ChatViewState) { renderedStates.add(viewState) } } init { presenter.attachView(view) } VIEW ROBOT subjects to control intents 62
  97. val renderedStates = mutableListOf<ChatViewState>() val initSubject = PublishSubject.create<ConversationId>() val sendMessageSubject

    = PublishSubject.create<Message>() val openProfileSubject = PublishSubject.create<ProfileId>() val view: ChatContract.View = object : ChatContract.View { override fun initIntent(): Observable<Long> = initSubject override fun sendMessageIntent(): Observable<Message> = sendMessageSubject override fun openProfileIntent(): Observable<ProfileId> = openProfileSubject override fun render(viewState: ChatViewState) { renderedStates.add(viewState) } } init { presenter.attachView(view) } VIEW ROBOT subjects to control intents view states are added to list 62
  98. val renderedStates = mutableListOf<ChatViewState>() val initSubject = PublishSubject.create<ConversationId>() val sendMessageSubject

    = PublishSubject.create<Message>() val openProfileSubject = PublishSubject.create<ProfileId>() val view: ChatContract.View = object : ChatContract.View { override fun initIntent(): Observable<Long> = initSubject override fun sendMessageIntent(): Observable<Message> = sendMessageSubject override fun openProfileIntent(): Observable<ProfileId> = openProfileSubject override fun render(viewState: ChatViewState) { renderedStates.add(viewState) } } init { presenter.attachView(view) } VIEW ROBOT subjects to control intents view states are added to list 62
  99. on("send message") { val presenter = ChatPresenter(useCase, TestSchedulersFacade()) val robot

    = ChatViewRobot(presenter) robot.start(conversationId) it("should send message and update list") { robot.sendMessage(message) val expectedState = ViewState(items = items.plus(message)) robot.assertViewStatesRendered { listOf( initState, fetchedMessagesState, messageSendingState, messageSentState, expectedState ) } } } VIEW ROBOT 63
  100. VIEW ROBOT • Abstraction for your view • Can be

    used in MVI, MVVM, MVP • Robot records state changes
  101. VIEW ROBOT • Abstraction for your view • Can be

    used in MVI, MVVM, MVP • Robot records state changes • Then you can test validate full flow in fast test
  102. • test + specification -> system documentation • helps you

    understand product better SPECIFICATIONS 65
  103. • test + specification -> system documentation • helps you

    understand product better • fits great with TDD and BDD SPECIFICATIONS 65
  104. • test + specification -> system documentation • helps you

    understand product better • fits great with TDD and BDD • makes it easier to separate implementation from behavior SPECIFICATIONS 65
  105. KOTLINTEST SPECIFICATION class DistanceConverterSpec : BehaviorSpec({ Given("some preconditions"){ When("execute action"){

    Then("perform assertions"){ } Then("perform other assertion"){ } } When("execute other action"){ Then("check and perform assertion"){ throw AssertionError() } } } }) 66
  106. 69

  107. 69

  108. SPEK SPECIFICATION val converter by memoized(mode = CachingMode.EACH_GROUP){ DistanceConverter() }

    val converter by memoized(mode = CachingMode.TEST){ DistanceConverter() } REUSE OBJECT BETWEEN TESTS CAREFULLY! 70
  109. • Know your frameworks! • Keep unit test isolated •

    Test behavior, not implementation SUMMARY 71
  110. • Know your frameworks! • Keep unit test isolated •

    Test behavior, not implementation • Make use of language features (infix notation, lambdas, extension functions) SUMMARY 71
  111. • Know your frameworks! • Keep unit test isolated •

    Test behavior, not implementation • Make use of language features (infix notation, lambdas, extension functions) • Structure your tests well SUMMARY 71