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

The Nitty Gritty of Unit Testing in Kotlin

Yun Cheng
May 31, 2019
28

The Nitty Gritty of Unit Testing in Kotlin

@ mDevCamp, Prague, CZ May 2019

Yun Cheng

May 31, 2019
Tweet

Transcript

  1. Add Kotlin into an existing app Google recommends a gradual

    approach: 1. Start by writing tests in Kotlin. 2. Write new code in Kotlin. 3. Update existing code to Kotlin.
  2. Nitty gritty details What mocking libraries to use?? Kotlin classes

    are final by default?? lateinit var when is a keyword in Kotlin?? Null safety in Kotlin??
  3. About our apps • Model View Presenter ◦ UI architecture

    • JUnit ◦ Testing framework • Mockito ◦ Mocking framework • RxJava ◦ Asynchronous event handling
  4. Quick MVP recap • Contains all the logic for manipulating

    data ◦ movieDatabse.insert(movie) • Activity class - contains all the views and view logic ◦ private lateinit var titleEditText: EditText ◦ titleEditText.setText(data?.getStringExtra(EXTRA_TITLE)) • The brain that controls what the Model and View does ◦ model.insert(movie) ◦ view.displayError("Movie title cannot be empty") Model Presenter View
  5. Presenter class class AddMoviePresenter(private var view: AddMovieContract.ViewInterface, private var model:

    LocalDataSource) : AddMovieContract.PresenterInterface { override fun addMovie(title: String, releaseDate: String, posterPath: String) { } }
  6. Presenter class class AddMoviePresenter(private var view: AddMovieContract.ViewInterface, private var model:

    LocalDataSource) : AddMovieContract.PresenterInterface { override fun addMovie(title: String, releaseDate: String, posterPath: String) { if (title.isEmpty()) { view.displayError("Movie title cannot be empty") } else { val movie = Movie(title, releaseDate, posterPath) model.insert(movie) view.returnToMain() } } }
  7. Test class @RunWith(MockitoJUnitRunner::class) class AddMoviePresenterTests { @Mock private val mockView

    : AddMovieContract.ViewInterface @Mock private val mockModel : LocalDataSource private lateinit var presenter : AddMoviePresenter //Add tests here } Makes it easy to use the @Mock annotation to create mocks
  8. Test class @RunWith(MockitoJUnitRunner::class) class AddMoviePresenterTests { @Mock private lateinit var

    mockView : AddMovieContract.ViewInterface @Mock private lateinit var mockModel : LocalDataSource private lateinit var presenter : AddMoviePresenter //Add tests here }
  9. Example unit test @Test fun testAddMovieWithTitle() { presenter.addMovie("The Lion King",

    "1994-05-07", "/lionking.jpg") Mockito.verify(mockModel).insert(argumentCaptor.capture()) assertEquals("The Lion King", argumentCaptor.value.title) }
  10. Example unit test @Captor private lateinit var argumentCaptor: ArgumentCaptor<Movie> @Test

    fun testAddMovieWithTitle() { presenter.addMovie("The Lion King", "1994-05-07", "/lionking.jpg") Mockito.verify(mockModel).insert(argumentCaptor.capture()) assertEquals("The Lion King", argumentCaptor.value.title) Mockito.verify(mockView).returnToMain() }
  11. How to mock Kotlin classes? • Kotlin classes are final

    by default • Final classes can’t be mocked (either manually or with Mockito 1)
  12. Solutions 1. Use open keyword to open the kotlin class

    ◦ open class LocalDataSource(application: Application) { private val movieDao: MovieDao open fun insert(movie: Movie) { thread { movieDao.insert(movie) } } }
  13. Solutions 1. Use open keyword to open the kotlin class

    ◦ open class LocalDataSource(application: Application) { private val movieDao: MovieDao open fun insert(movie: Movie) { thread { movieDao.insert(movie) } } } 2. Use interfaces ◦ Comes free with MVP! Interfaces in Kotlin are NOT final!
  14. Solutions 1. Use open keyword to open the kotlin class

    ◦ open class LocalDataSource(application: Application) { private val movieDao: MovieDao open fun insert(movie: Movie) { thread { movieDao.insert(movie) } } } 2. Use interfaces ◦ Comes free with MVP! 3. Use a library: mockK, mockito-kotlin, Mockito 2
  15. Mockito 2 solution 1. In build.gradle, upgrade to Mockito 2:

    ◦ testImplementation 'org.mockito:mockito-core:2.2.5' 2. Create a new file src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker 3. Add this single line to the file: ◦ mock-maker-inline
  16. Mockito 2 alternate solution 1. Starting with version 2.7.6, there

    is the 'mockito-inline' artifact ◦ testImplementation 'org.mockito:mockito-inline:2.7.6' 2. That’s it!
  17. Example unit test @Test fun testAddMovieWithTitle() { //Invoke presenter.addMovie("The Lion

    King", "1994-05-07", "/lionking.jpg") //Verify Mockito.verify(mockModel).insert(argumentCaptor.capture()) assertEquals("The Lion King", argumentCaptor.value.title) Mockito.verify(mockView).returnToMain() }
  18. Mockito Matchers & ArgumentCaptors Matchers like any(…), eq(), and capture()

    often return null From docs: By default, for all methods that return value, mock returns null, an empty collection or appropriate primitive/primitive wrapper value (e.g: 0, false, ... for int/Integer, boolean/Boolean, ...) In Kotlin, this results in NPEs at runtime: java.lang.IllegalStateException: movieArgumentCaptor.capture() must not be null
  19. Mockito Matchers & ArgumentCaptors Matchers like any(…), eq(), and capture()

    often return null From docs: By default, for all methods that return value, mock returns null, an empty collection or appropriate primitive/primitive wrapper value (e.g: 0, false, ... for int/Integer, boolean/Boolean, ...) Mockito.verify(mock).someMethod(anyInt(), any(), eq("my third argument")); returns 0 returns null returns null
  20. MockitoKotlinHelpers.kt fun <T> eq(obj: T): T = Mockito.eq<T>(obj) fun <T>

    any(): T = Mockito.any<T>() fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
  21. MockitoKotlinHelpers.kt: ArgumentCaptor /** * Returns ArgumentCaptor.capture() as nullable type to

    avoid * java.lang.IllegalStateException when null is returned. */ fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
  22. Generic T is nullable bc implicitly bounded by Any? fun

    <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture() is the same as fun <T> capture(argumentCaptor: ArgumentCaptor<T: Any?>): T = argumentCaptor.capture() Return type is also Any?
  23. Example unit test @Test fun testAddMovieWithTitle() { presenter.addMovie("The Lion King",

    "1994-05-07", "/lionking.jpg") Mockito.verify(mockModel).insert(capture(argumentCaptor)) assertEquals("The Lion King", argumentCaptor.value.title) Mockito.verify(mockView).returnToMain() }
  24. Another example unit test Code: @get:Query("SELECT * FROM movie_table") val

    allMovies: Observable<List<Movie>> Unit Test: @Test fun testGetMyMoviesList() { when(mockModel.allMovies).thenReturn(Observable.just(myDummyMovies)) ... }
  25. Another example unit test Code: @get:Query("SELECT * FROM movie_table") val

    allMovies: Observable<List<Movie>> Unit Test: @Test fun testGetMyMoviesList() { when(mockModel.allMovies).thenReturn(Observable.just(myDummyMovies)) ... } Unresolved reference: thenReturn
  26. Solution: Escaping syntax required to use `when` Code: @get:Query("SELECT *

    FROM movie_table") val allMovies: Observable<List<Movie>> Unit Test: @Test fun testGetMyMoviesList() { `when`(mockModel.allMovies).thenReturn(Observable.just(myDummyMovies)) }
  27. Directory structure Old Eclipse directory structure • Didn’t have a

    src/main, but instead used src/ as our main source directory Gradle wasn’t able to identify where the tests were • Could only run either kotlin or java tests sourceSets { main.kotlin.srcDirs = ['src/main/kotlin', 'src/main/java'] main.java.srcDirs = [] test.kotlin.srcDirs = ['src/test/kotlin', 'src/test/java'] Test.java.srcDirs = ['src/test/kotlin', 'src/test/java'] }
  28. Can write function names with spaces in them • Using

    backticks will allow you to write tests with spaces in them @Test fun `test getReleaseYear from string date`() { // } @Test fun `test getReleaseYear from string year`() { // } @Test fun `test getReleaseYear from empty date`() { // } @Test fun `test getReleaseYear from null date`() { // }
  29. Can nest test methods in groups • Use the @Nested

    annotation to group test methods (requires JUnit 5) @Nested inner class TypicalTests { @Test fun `test getReleaseYear from string formatted date`() { // } @Test fun `test getReleaseYear from year`() { // } } @Nested inner class EdgeCaseTests { @Test fun `test getReleaseYear from empty date`() { // } @Test fun `test getReleaseYear from null date`() { // } }
  30. Summary • Properties in test class need to be var

    lateinit • Kotlin classes are final by default ◦ open keyword, interfaces, libraries • Use helper functions for Mockito Matchers and ArgumentCaptors ◦ MockitoKotlinHelpers.kt • Use backticks for `when` • Use the default Android Studio directory structure • Use backticks around test names if you want to include spaces • Group tests together using @nested (requires JUnit 5)