Improve your tests using Kotlin.

Improve your tests using Kotlin.

71b4edb678001f55b47bdde0741a0ff5?s=128

Fábio Carballo

October 12, 2017
Tweet

Transcript

  1. Improve your tests using Kotlin Fábio Carballo @fabiocarballo

  2. Why Kotlin?…

  3. • Immutability first

  4. • Immutability first • Hides Nullability

  5. • Immutability first • Hides Nullability • Allows extensions to

    third-party classes
  6. • Immutability first • Hides Nullability • Allows extensions to

    third-party classes • High-order functions / Lambdas
  7. Still afraid?… Use Kotlin in your tests.

  8. • Mockito testCompile 'org.mockito:mockito-core:2.8.9' • Kotlin JUnit Assertions testCompile org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion}

  9. class Greeter( private val user: User) { fun getEnglishGreeting() =

    "Hello, ${user.fullName()}!" } class User( private val firstName: String, private val lastName: String) { fun fullName(): String = "$firstName $lastName" }
  10. class GreeterTest { @Mock lateinit var user: User lateinit var

    tested: Greeter @Before fun setUp() { MockitoAnnotations.initMocks(this) tested = Greeter(user) } @Test fun englishGreetIsCorrect() { Mockito.`when`(user.fullName()).thenReturn("Fábio Carballo") assertEquals("Hello, Fábio Carballo!", tested.getEnglishGreeting()) } }
  11. class GreeterTest { @Mock lateinit var user: User lateinit var

    tested: Greeter @Before fun setUp() { MockitoAnnotations.initMocks(this) tested = Greeter(user) } @Test fun englishGreetIsCorrect() { Mockito.`when`(user.fullName()).thenReturn("Fábio Carballo") assertEquals("Hello, Fábio Carballo!", tested.getEnglishGreeting()) } } org.mockito.exceptions.base.MockitoException: Cannot mock/spy class fabiocarballo.com.myapplication.User Mockito cannot mock/spy because : - final class
  12. • Use Interfaces interface User { fun fullName(): String }

  13. • “Open” Classes open class User( private val firstName: String,

    private val lastName: String) { open fun fullName(): String = "$firstName $lastName" }
  14. • Use Mockito 2.2.X

  15. Mockito.verify(user).addFriend(Mockito.any()) class User( private val firstName: String, private val lastName:

    String) { fun addFriend(friend: User) { … } }
  16. Mockito.verify(user).addFriend(Mockito.any()) class User( private val firstName: String, private val lastName:

    String) { fun addFriend(friend: User) { … } } Mockito Matchers rely on null-values.
  17. Mockito.verify(user).addFriend(Mockito.any()) class User( private val firstName: String, private val lastName:

    String) { fun addFriend(friend: User) { … } } Mockito Matchers rely on null-values. Mockito.verify(user).addFriend(null)
  18. mockito-kotlin https://github.com/nhaarman/mockito-kotlin

  19. mockito-kotlin verify(user).addFriend(any()) anyOrNull() anyVarArg() anyArray()

  20. mockito-kotlin Mockito.`when`(user.fullName()).thenReturn(“Fábio Carballo”)

  21. mockito-kotlin Mockito.`when`(user.fullName()).thenReturn(“Fábio Carballo”) whenever(user.fullName()).thenReturn(“Fábio Carballo”)

  22. mockito-kotlin val user: User = Mockito.mock(User::class)

  23. mockito-kotlin val user: User = Mockito.mock(User::class) val user: User =

    mock()
  24. mockito-kotlin val user: User = Mockito.mock(User::class) val user: User =

    mock() val user: User = mock { on { fullName() }.thenReturn(“Fábio Carballo”) }
  25. class GreeterTest { @Mock lateinit var user: User lateinit var

    tested: Greeter @Before fun setUp() { MockitoAnnotations.initMocks(this) tested = Greeter(user) } @Test fun englishGreetIsCorrect() { Mockito.`when`(user.fullName()).thenReturn("Fábio Carballo") assertEquals("Hello, Fábio Carballo!", tested.getEnglishGreeting()) } }
  26. class GreeterTest { val user: User = mock() val tested

    = Greeter(user) @Test fun englishGreetIsCorrect() { whenever(user.fullName()).thenReturn("Fábio Carballo") assertEquals("Hello, Fábio Carballo!", tested.getEnglishGreeting()) } }
  27. class GreeterTest { val user: User = mock() val tested

    = Greeter(user) @Test fun englishGreetIsCorrect() { whenever(user.fullName()).thenReturn("Fábio Carballo") assertEquals("Hello, Fábio Carballo!", tested.getEnglishGreeting()) } } class GreeterTest { val user: User = mock { on { fullName() }.thenReturn("Fábio Carballo") } val tested = Greeter(user) @Test fun englishGreetIsCorrect() { assertEquals("Hello, Fábio Carballo!", tested.getEnglishGreeting()) } }
  28. Kotlin Quick-Wins

  29. @Test fun test_addFriend_userHasNoFriends_FriendsListShouldNotBeEmpty() { … }

  30. @Test fun test_addFriend_userHasNoFriends_FriendsListShouldNotBeEmpty() { … } @Test fun `when user

    has no friends and a friend is added the friends list should not be empty`() { … }
  31. class ServiceA { fun methodA() { … } } class

    ServiceB { fun methodB() { … } } class Test { fun test() { val serviceA: ServiceA = mock() val serviceB: ServiceB = mock() serviceA.methodA() serviceB.methodB() val inOrder = inOrder(serviceA, serviceB) inOrder.verify(serviceA).methodA() inOrder.verify(serviceB).methodB() } }
  32. class ServiceA { fun methodA() { … } } class

    ServiceB { fun methodB() { … } } class Test { fun test() { val serviceA: ServiceA = mock() val serviceB: ServiceB = mock() serviceA.methodA() serviceB.methodB() val inOrder = inOrder(serviceA, serviceB) inOrder.verify(serviceA).methodA() inOrder.verify(serviceB).methodB() } }
  33. class ServiceA { fun methodA() { … } } class

    ServiceB { fun methodB() { … } } class Test { fun test() { val serviceA: ServiceA = mock() val serviceB: ServiceB = mock() serviceA.methodA() serviceB.methodB() inOrder(serviceA, serviceB).apply { verify(serviceA).methodA() verify(serviceB).methodB() } } }
  34. kluent https://github.com/MarkusAmshove/Kluent expekt https://github.com/winterbe/expekt

  35. data class User( private val firstName: String, private val lastName:

    String, private val age: Int) { val fullName = "$firstName $lastName" val canDrink = (age >= 18) val friends: MutableList<User> = mutableListOf() fun addFriend(user: User) { if (user !in friends) { friends.add(user) user.addFriend(this) } } }
  36. @Test fun `the full name is the concatenation of the

    first and last name`() { val user = User(firstName = "Fábio", lastName = "Carballo", age = 26) assertEquals("Fábio Carballo", user.fullName) }
  37. @Test fun `the full name is the concatenation of the

    first and last name`() { val user = User(firstName = "Fábio", lastName = "Carballo", age = 26) assertEquals("Fábio Carballo", user.fullName) user.fullName `should equal` "Fábio Carballo” // Kluent }
  38. @Test fun `the full name is the concatenation of the

    first and last name`() { val user = User(firstName = "Fábio", lastName = "Carballo", age = 26) assertEquals("Fábio Carballo", user.fullName) user.fullName `should equal` "Fábio Carballo” // Kluent } infix fun Any.`should equal`(theOther: Any) = assertEquals(theOther, this)
  39. @Test fun `the full name is the concatenation of the

    first and last name`() { val user = User(firstName = "Fábio", lastName = "Carballo", age = 26) assertEquals("Fábio Carballo", user.fullName) user.fullName `should equal` "Fábio Carballo” // Kluent user.fullName.should.equal(“Fábio Carballo”) // Expect }
  40. @Test fun `friends are correctly added`() { val rui =

    User(firstName = "Rui", lastName = "Gonçalo", age = 28) val fabio = User(firstName = "Fábio", lastName = "Carballo", age = 26) rui.addFriend(fabio) assertEquals(listOf(fabio), rui.friends) // OR assertTrue { fabio in rui.friends } }
  41. @Test fun `friends are correctly added`() { val rui =

    User(firstName = "Rui", lastName = "Gonçalo", age = 28) val fabio = User(firstName = "Fábio", lastName = "Carballo", age = 26) rui.addFriend(fabio) assertEquals(listOf(fabio), rui.friends) // KLUENT rui.friends `should contain` fabio rui.friends `should not contain` rui }
  42. @Test fun `friends are correctly added`() { val rui =

    User(firstName = "Rui", lastName = "Gonçalo", age = 28) val fabio = User(firstName = "Fábio", lastName = "Carballo", age = 26) rui.addFriend(fabio) assertEquals(listOf(fabio), rui.friends) // KLUENT rui.friends `should contain` fabio rui.friends `should not contain` rui // EXPEKT rui.friends.should.contain(fabio) }
  43. @Test fun `friends are correctly added`() { val rui =

    User(firstName = "Rui", lastName = "Gonçalo", age = 28) val fabio = User(firstName = "Fábio", lastName = "Carballo", age = 26) rui.addFriend(fabio) assertEquals(listOf(fabio), rui.friends) // KLUENT rui.friends `should contain` fabio rui.friends `should not contain` rui // EXPEKT rui.friends.should.contain(fabio) fabio.friends.should.have.all.elements(rui).and.have.size(1) }
  44. @Test fun `friends are correctly added`() { val rui =

    User(firstName = "Rui", lastName = "Gonçalo", age = 28) val fabio = User(firstName = "Fábio", lastName = "Carballo", age = 26) rui.addFriend(fabio) assertEquals(listOf(fabio), rui.friends) // KLUENT rui.friends `should contain` fabio rui.friends `should not contain` rui // EXPEKT rui.friends.should.contain(fabio) fabio.friends.should.have.all.elements(rui).and.have.size(1) expect(rui.friends).to.contain(fabio) }
  45. fun drink() { if (canDrink) { System.out.println("Drinking .. ") }

    else { throw UnderageDrinkingException() } } class UnderageDrinkingException() : Exception("You can't drink.")
  46. fun drink() { if (canDrink) { System.out.println("Drinking .. ") }

    else { throw UnderageDrinkingException() } } class UnderageDrinkingException() : Exception("You can't drink.") @Test(expected = UnderageDrinkingException::class) fun `an underage user can't drink`() { val ricardo = User( firstName = "Ricardo", lastName = "Trindade", age = 17) ricardo.drink() }
  47. fun drink() { if (canDrink) { System.out.println("Drinking .. ") }

    else { throw UnderageDrinkingException() } } class UnderageDrinkingException() : Exception("You can't drink.") fun `an underage user can't drink`() { val ricardo = User( firstName = "Ricardo", lastName = "Trindade", age = 17) val ricardoDrinks = { ricardo.drink() }
 
 ricardoDrinks `should throw` UnderageDrinkingException::class }
  48. fun drink() { if (canDrink) { System.out.println("Drinking .. ") }

    else { throw UnderageDrinkingException() } } class UnderageDrinkingException() : Exception("You can't drink.") fun `an underage user can't drink`() { val ricardo = User( firstName = "Ricardo", lastName = "Trindade", age = 17) val ricardoDrinks = { ricardo.drink() }
 
 ricardoDrinks `should throw the Exception` UnderageDrinkingException::class `with message` "You can't drink." }
  49. Spek https://github.com/spekframework/spek

  50. • JUnit tests have a flat structure • RSpec •

    TDD test framework for Kotlin
  51. @RunWith(JUnitPlatform::class) class SimpleSpec : Spek({ it("should be true") { true

    `should be` true } })
  52. Hangman •Player 1 thinks about an arbitrary word. •Other players

    try to guess the word by suggesting a letter. •Each wrong guess, a part of the hangman is drawn.
  53. class Hangman(private val word: String) { var wrongAnswerCount = 0

    fun suggestLetter(letter: Char): Boolean { … } }
  54. • Feature description • describe -> it

  55. @RunWith(JUnitPlatform::class) class HangmanFeatureSpec : Spek({ val game = Hangman("apple") describe("letter

    verification") { it("returns true when the suggestion is correct") { game.suggestLetter('a') `should equal` true } it("returns false when the suggestion is incorrect") { game.suggestLetter('f') `should equal` false } } })
  56. @RunWith(JUnitPlatform::class) class HangmanFeatureSpec : Spek({ val game = Hangman("apple") describe("letter

    verification") { it("returns true when the suggestion is correct") { game.suggestLetter('a') `should equal` true } it("returns false when the suggestion is incorrect") { game.suggestLetter('f') `should equal` false } } })
  57. • Behavior description • given -> on -> it

  58. @RunWith(JUnitPlatform::class) class HangmanBehaviorSpec : Spek({ val hiddenWord = "apple" given("a

    new game with the hidden word being $hiddenWord:") { val game = Hangman(hiddenWord) on(“correct suggestion") { game.suggestLetter(‘a’) it("has no wrong answers") { game.wrongAnswerCount `should equal` 0 } it("shows the guessed letters") { game.dashedWord `should equal` "a----" } } } })
  59. @RunWith(JUnitPlatform::class) class HangmanBehaviorSpec : Spek({ val hiddenWord = "apple" given("a

    new game with the hidden word being $hiddenWord:") { val game = Hangman(hiddenWord) on(“correct suggestion") { game.suggestLetter(‘a’) it("has no wrong answers") { game.wrongAnswerCount `should equal` 0 } it("shows the guessed letters") { game.dashedWord `should equal` "a----" } } } })
  60. Scopes • Tests (it)

  61. Scopes • Tests (it) • Group (given, describe, context, group)

  62. fun SpecBody.describe(description: String, body: SpecBody.() -> Unit) { group("describe $description",

    body = body) } fun SpecBody.context(description: String, body: SpecBody.() -> Unit) { group("context $description", body = body) } fun SpecBody.given(description: String, body: SpecBody.() -> Unit) { group(“given $description", body = body) }
  63. Scopes • Tests (it) • Group (given, describe, context, group)

    • Action (on)
  64. Ignoring tests • Just add an X as a prefix

    to the scope xit("returns true when the suggestion is correct") { game.suggestLetter('a') `should equal` true }
  65. Ignoring tests • Just add an X as a prefix

    to the scope xit("returns true when the suggestion is correct") { game.suggestLetter('a') `should equal` true }
  66. Thank you. Questions?