Best Practices for Unit Testing in Kotlin @philipp_hauer - - PowerPoint PPT Presentation

best practices for unit testing in kotlin
SMART_READER_LITE
LIVE PREVIEW

Best Practices for Unit Testing in Kotlin @philipp_hauer - - PowerPoint PPT Presentation

Best Practices for Unit Testing in Kotlin @philipp_hauer Spreadshirt KotlinConf, Amsterdam Oct 05, 2018 Question My First Test in Kotlin... open class UserRepository Bolla! class UserControllerTest { op


slide-1
SLIDE 1

Best Practices for Unit Testing in Kotlin

@philipp_hauer Spreadshirt KotlinConf, Amsterdam

Oct 05, 2018

slide-2
SLIDE 2

Question

slide-3
SLIDE 3

My First Test in Kotlin...

slide-4
SLIDE 4

4

  • pen class UserRepository

class UserControllerTest { companion object { @JvmStatic lateinit var controller: UserController @JvmStatic lateinit var repo: UserRepository @BeforeClass @JvmStatic initialize() { repo = mock() controller = UserController(repo) } } @Test fun findUser_UserFoundAndHasCorrectValues() { `when`(repo.findUser(1)).thenReturn(User(1, "Peter")) val user = controller.getUser(1) assertEquals(user?.name, "Peter") } }

mul! regal! bu, sac!

  • p qu

Har Rad! Por Er Meg Bet Moc AP? Bolla!

slide-5
SLIDE 5

5

We can do better! Rede Cle Idiic Con Resal Fi Mess

slide-6
SLIDE 6

6

How? Tes cle Tes ri Nam, Grog Moc dg The of Dat se Spig Iegon

slide-7
SLIDE 7

Recap: Idiomatic Kotlin Code

slide-8
SLIDE 8

Idiomatic Kotlin Code

8

Immutability Non-Nullability No Static Access

val var String String?

No direct language feature

slide-9
SLIDE 9

Test Class Lifecycle

slide-10
SLIDE 10

class RepositoryTest { }

JUnit4: Always New Test Class Instances

10

@Test fun test1() { ... } @Test fun test2() { ... } instance1: RepositoryTest instance2: RepositoryTest

Where to put the initial setup code?

val mongo = startMongoContainer()

Exed o ec te

slide-11
SLIDE 11

JUnit4: Static for the Initial Setup Code

11

class RepositoryTest { companion object { @JvmStatic private lateinit var mongo: GenericContainer @JvmStatic private lateinit var repo: Repository @BeforeClass @JvmStatic fun initialize() { mongo = startMongoContainer() repo = Repository(mongo.host, mongo.port) } } }

mul sac Bolla! nu woru

slide-12
SLIDE 12

JUnit5 to the Rescue!

12

slide-13
SLIDE 13

JUnit5: Reuse the Test Class Instance

13

@TestInstance(TestInstance.Lifecycle.PER_CLASS) class RepositoryTest { } private val mongo = startMongoContainer().apply { configure() } private val repo = Repository(mongo.host, mongo.port) @Test fun test1() { }

Con Idiic

slide-14
SLIDE 14

JUnit5: Reuse the Test Class Instance

14

@TestInstance(TestInstance.Lifecycle.PER_CLASS) class RepositoryTest { private val mongo: GenericContainer private val repo: Repository init { mongo = startMongoContainer().apply { configure() } repo = Repository(mongo.host, mongo.port) } }

slide-15
SLIDE 15

JUnit5: Change the Lifecycle Default

15

junit.jupiter.testinstance.lifecycle.default = per_class

src/test/resources/junit-platform.properties:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

slide-16
SLIDE 16

Test Names and Grouping

slide-17
SLIDE 17

Backticks

17

class TagClientTest { @Test fun `basic tag list`() {} @Test fun `empty tag list`() {} }

slide-18
SLIDE 18

18

Whi t bog to c to?

slide-19
SLIDE 19

@Nested Inner Classes

19

class DesignControllerTest { @Nested inner class GetDesigns { @Test fun `all fields are included`() {} @Test fun `limit parameter`() {} } @Nested inner class DeleteDesign { @Test fun `design is removed in db`() {} } }

getDesign() deleteDesign()

slide-20
SLIDE 20

20

slide-21
SLIDE 21

Kotlin Test Libraries

slide-22
SLIDE 22

Being Spoilt for Choice

22

Incomplete list. Some libraries fit into multiple categories.

Test Frameworks Mocking Assertions Kotlin Java

Spek KotlinTest JUnit5 Mockito-Kotlin MockK Strikt Atrium HamKrest Expekt Kluent AssertK AssertJ

My es co (fo w)

slide-23
SLIDE 23

Test-Specific Extension Functions

23

assertThat(taxRate1).isCloseTo(0.3f, Offset.offset(0.001f)) assertThat(taxRate2).isCloseTo(0.2f, Offset.offset(0.001f)) assertThat(taxRate3).isCloseTo(0.5f, Offset.offset(0.001f)) fun AbstractFloatAssert<*>.isCloseTo(expected: Float) = this.isCloseTo(expected, Offset.offset(0.001f))

// Usage:

assertThat(taxRate1).isCloseTo(0.3f) assertThat(taxRate2).isCloseTo(0.2f) assertThat(taxRate3).isCloseTo(0.5f)

Dupti Cle Idiic

slide-24
SLIDE 24

Mock Handling

slide-25
SLIDE 25

Classes Are Final by Default

25

Solutions

  • Interfaces
  • open explicitly
  • Mockito: Enable incubating feature to

mock final classes

  • MockK
slide-26
SLIDE 26

MockK

26

val clientMock: UserClient = mockk() every { clientMock.getUser(any()) } returns User(id = 1, name = "Ben") val updater = UserUpdater(clientMock) updater.updateUser(1) verify { clientMock.getUser(1) } mockk(relaxed=true)

slide-27
SLIDE 27

MockK

27

java.lang.AssertionError: Verification failed: calls are not exactly matching verification sequence Matchers: UserClient(#5).getUser(eq(2))) UserRepo(#4).saveUser(eq(User(id=1, name=Ben, age=29)))) Calls: 1) UserClient(#5).getUser(1) 2) UserRepo(#4).saveUser(User(id=1, name=Ben, age=29)) verifySequence { clientMock.getUser(2) repoMock.saveUser(user) }

slide-28
SLIDE 28

Does Test Speed Matter?

28

2 s o 31 Uni Tt?

slide-29
SLIDE 29

Don't Recreate Mocks

29

class DesignControllerTest { private lateinit var repo: DesignRepository private lateinit var client: DesignClient private lateinit var controller: DesignController @BeforeEach fun init() { repo = mockk() client = mockk() controller = DesignController(repo, client) } }

Exes!

slide-30
SLIDE 30

Create Mocks Once, Reset Them

30

class DesignControllerTest { private val repo: DesignRepository = mockk() private val client: DesignClient = mockk() private val controller = DesignController(repo, client) @BeforeEach fun init() { clearMocks(repo, client) } }

Fas

slide-31
SLIDE 31

Create Mocks Once, Reset Them

31

2.1 s 0.4 s

slide-32
SLIDE 32

Handle Classes with State

32

class DesignViewTest { private val repo: DesignRepository = mockk() private lateinit var view: DesignView @BeforeEach fun init() { clearMocks(repo) view = DesignView(repo) } @Test fun changeButton() { assertThat(view.button.caption).isEqualTo("Hi") view.changeButton() assertThat(view.button.caption).isEqualTo("Guten Tag") } }

saf re-ceon ir

slide-33
SLIDE 33

Spring Integration

slide-34
SLIDE 34

All-Open Compiler Plugin

34

@Configuration class SpringConfiguration{ @Bean fun objectMapper() = ObjectMapper().registerKotlinModule() }

BeanDefinitionParsingException: Configuration problem: @Configuration class 'SpringConfiguration' may not be final.

<dependency> <groupId>org.jetbrains.kotlin</groupId> <artifactId>kotlin-maven-allopen</artifactId> <version>${kotlin.version}</version> </dependency> <compilerPlugins> <plugin>spring</plugin> </compilerPlugins>

slide-35
SLIDE 35

Constructor Injection for Spring-free Testing

35

@Component class DesignController( private val designRepo: DesignRepository, private val designClient: DesignClient, ) {}

Eas t Log to Spg:

val repo: DesignRepository = mockk() val client: DesignClient = mockk() val controller = DesignController(repo, client)

slide-36
SLIDE 36

Utilize Data Classes

slide-37
SLIDE 37
  • rg.junit.ComparisonFailure: expected:<[2]> but was:<[1]>

Expected :2 Actual :1

Data Classes for Assertions

37

assertThat(actualDesign.id).isEqualTo(2) assertThat(actualDesign.userId).isEqualTo(9) assertThat(actualDesign.name).isEqualTo("Cat")

???

slide-38
SLIDE 38

Data Classes for Assertions

38

val expectedDesign = Design(id = 2, userId = 9, name = "Cat") assertThat(actualDesign).isEqualTo(expectedDesign)

  • rg.junit.ComparisonFailure: expected:<Design(id=[2], userId=9,

name=Cat...> but was:<Design(id=[1], userId=9, name=Cat...> Expected :Design(id=2, userId=9, name=Cat) Actual :Design(id=1, userId=9, name=Cat)

se-exnoy

slide-39
SLIDE 39

Expecting: <[Design(id=1, userId=9, name=Cat), Design(id=2, userId=4, name=Dogggg)]> to contain exactly (and in same order): <[Design(id=1, userId=9, name=Cat), Design(id=2, userId=4, name=Dog)]> but some elements were not found: <[Design(id=2, userId=4, name=Dog)]> and others were not expected: <[Design(id=2, userId=4, name=Dogggg)]>

Data Classes for Assertions

39

assertThat(actualDesigns).containsExactly( Design(id = 1, userId = 9, name = "Cat"), Design(id = 2, userId = 4, name = "Dog") )

Gre!

slide-40
SLIDE 40

Data Classes for Assertions

40

assertThat(actualDesign) .isEqualToIgnoringGivenFields(expectedDesign,"id") assertThat(actualDesigns) .usingElementComparatorIgnoringFields("id") .containsExactly(expectedDesign1, expectedDesign2) assertThat(actualDesigns) .usingElementComparatorOnFields("name") .containsExactly(expectedDesign1, expectedDesign2) assertThat(actualDesign) .isEqualToComparingOnlyGivenFields(expectedDesign,"name")

Lists Single Element

slide-41
SLIDE 41

Helper Function for Object Creation

41

val testDesign = Design( id = 1, userId = 9 name = "Fox", dateCreated = Instant.now(), tags = mapOf() ) val testDesign2 = Design( id = 2, userId = 9 name = "Cat", dateCreated = Instant.now(), tags = mapOf() )

  • Blo ce
  • Are ps ev

fo h t?

slide-42
SLIDE 42

Helper Function for Object Creation

42

fun createDesign( id: Int = 1, name: String = "Cat", date: Instant = Instant.ofEpochSecond(1518278198), tags: Map<Locale, List<Tag>> = mapOf( Locale.US to listOf(Tag(value = "$name in English")), ) ) = Design( id = id, userId = 9, name = name, dateCreated = date, tags = tags ) // Usage: val testDesign = createDesign() val testDesign2 = createDesign( id = 1, name = "Fox" )

Con

slide-43
SLIDE 43

Helper Function for Object Creation

43

repo.saveAll( createDesign(isEnabled = true, language = Locale.US), createDesign(isEnabled = true, language = Locale.GERMANY), createDesign(isEnabled = false, language = Locale("nl","NL")) )

Tale Cri Fnin fo CrTes

CurrentTest.kt:

slide-44
SLIDE 44

Helper Function for Object Creation

44

fun createDesign( isEnabled: Boolean, language: Locale ) = createDesign( description = createDescription( translations = createTranslationsFor(language) ), state = if (isEnabled) createDisabledState() else createEnabledState() )

CreationUtils.kt CurrentTest.kt:

slide-45
SLIDE 45

Data Classes for Parameterized Tests

45

@Test fun `parse valid tokens`() { assertThat(parse("1511443755_2")).isEqualTo(Token(1511443755, "2")) assertThat(parse("151175_13521")).isEqualTo(Token(151175, "13521")) assertThat(parse("151144375_id")).isEqualTo(Token(151144375, "id")) assertThat(parse("1511443759_1")).isEqualTo(Token(1511443759, "1")) assertThat(parse(null)).isEqualTo(null) }

Whi n ad?

slide-46
SLIDE 46

Data Classes for Parameterized Tests

46

data class TestData( val input: String?, val expected: Token? )

slide-47
SLIDE 47

Data Classes for Parameterized Tests

47

@ParameterizedTest @MethodSource("validTokenProvider") fun `parse valid tokens`(testData: TestData) { assertThat(parse(testData.value)).isEqualTo(testData.expectedToken) } private fun validTokenProvider() = Stream.of( TestData(input = "1511443755_2", expected = Token(1511443755, "2")), TestData(input = "151175_13521", expected = Token(151175, "13521")), TestData(input = "151144375_id", expected = Token(151144375, "id")), TestData(input = "1511443759_1", expected = Token(1511443759, "1")), TestData(input = null, expected = null) )

slide-48
SLIDE 48

Conclusion

slide-49
SLIDE 49

49

  • pen class UserRepository

class UserControllerTest { companion object { @JvmStatic lateinit var controller: UserController @JvmStatic lateinit var repo: UserRepository @BeforeClass @JvmStatic initialize() { repo = mock() controller = UserController(repo) } } @Test fun findUser_UserFoundAndHasCorrectValues() { `when`(repo.findUser(1)).thenReturn(User(1, "Peter")) val user = controller.getUser(1) assertEquals(user?.name, "Peter") } }

slide-50
SLIDE 50

50

class UserControllerTest { private val repo: UserRepository = mockk() private val controller = UserController(repo) @Test fun `find user with correct values`() {

every { repo.findUser(1) } returns User(1, "Peter")

val user = controller.getUser(1) assertEquals(user).isEqualTo(User(1, "Peter")) } }

slide-51
SLIDE 51
slide-52
SLIDE 52

52

Data Classes FTW Best Practices for Testing in Kotlin

Equ Aset Creo Hl @ParerTes

Naming, Grouping

Bacc @Nes

Mock Handling

Don't ere; re! Moc

JUnit5 Kotlin

@TesIsac(PE_CAS)

Libraries

Cho or ge

slide-53
SLIDE 53

53

https://blog.philipphauer.de/best-practices-unit-testing-kotlin/

slide-54
SLIDE 54

Thank you!

@philipp_hauer Spreadshirt KotlinConf, Amsterdam

Oct 05, 2018

slide-55
SLIDE 55

Backup Slides

slide-56
SLIDE 56

Test-Specific Extension Functions

56

mvc.perform(get("designs/123?platform=$invalidPlatform")) .andExpect(status().isBadRequest) .andExpect(jsonPath("errorCode").value(code)) .andExpect(jsonPath("details", startsWith(msg))) fun ResultActions.andExpectErrorPage(code: Int, msg: String) = this.andExpect(status().isBadRequest) .andExpect(jsonPath("errorCode").value(code)) .andExpect(jsonPath("details", startsWith(msg)))

// Usage:

mvc.perform(get("designs/123?platform=$invalidPlatform")) .andExpectErrorPage(130, "Invalid platform.")

slide-57
SLIDE 57

Spring Integration

slide-58
SLIDE 58

Mock-based Spring Test Context

58

@ExtendWith(SpringExtension::class) @WebMvcTest(DesignController::class) @Import(TestConfig::class) class DesignControllerTest { @Autowired private lateinit var mvc: MockMvc @Autowired private lateinit var repoMock: DesignRepository @BeforeEach fun init() { clearMocks(repoMock) } @Test fun test() {} } @Configuration private class TestConfig { @Bean fun repoMock(): DesignRepository = mockk() }

slide-59
SLIDE 59

Spring Test Context for an Integration Test

59

@Configuration private class TestConfig { @Bean fun repo() = repo private val repo: DesignRepository init { val mongo = startMongoContainer() val mongoTemplate = createMongoTemplate(mongo.host, mongo.port) repo = DesignRepository(mongoTemplate) } }

Ini se vale Spg

slide-60
SLIDE 60

About Spreadshirt

slide-61
SLIDE 61

Spreadshirt

61

For two years