0% found this document useful (0 votes)
53 views61 pages

4 - Best Practices For Unit Testing in Kotlin

The document provides best practices for unit testing in Kotlin, including: 1) Using idiomatic Kotlin code structures like immutability, non-nullability, and avoiding static access in tests. 2) Leveraging JUnit 5 features like reusing the test class instance per class to reduce setup overhead. 3) Organizing tests with naming conventions, nested classes, and backticks for readability. 4) Utilizing Kotlin testing libraries for mocking, assertions, and other functionality.

Uploaded by

Kou Dev
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
53 views61 pages

4 - Best Practices For Unit Testing in Kotlin

The document provides best practices for unit testing in Kotlin, including: 1) Using idiomatic Kotlin code structures like immutability, non-nullability, and avoiding static access in tests. 2) Leveraging JUnit 5 features like reusing the test class instance per class to reduce setup overhead. 3) Organizing tests with naming conventions, nested classes, and backticks for readability. 4) Utilizing Kotlin testing libraries for mocking, assertions, and other functionality.

Uploaded by

Kou Dev
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 61

Best Practices

for Unit Testing


in Kotlin
@philipp_hauer
Spreadshirt
KotlinConf, Amsterdam
Oct 05, 2018
Question
My First Test in Kotlin...
open class UserRepository
Bo l {la !
class UserControllerTest op qu
companion object { mu l ! re g a l !
bu , @JvmStatic lateinit var controller: UserController
s a c! @JvmStatic lateinit var repo: UserRepository
@BeforeClass @JvmStatic initialize() {
repo = mock()
controller = UserController(repo)
}
}
@Test Har R ad!
fun findUser_UserFoundAndHasCorrectValues() {
Bet `when`(repo.findUser(1)).thenReturn(User(1, "Peter"))
Moc val user = controller.getUser(1)
AP ? assertEquals(user?.name, "Peter") Po r E r Me g
}
4
We can do better!

Re d e Idi ic

Cle Con
Re s a l F i Mes s
5
How?
Tes c le Nam , Gro g

Tes r i Moc d g

Sp i g I eg on The of
Dat se
6
Recap:
Idiomatic Kotlin Code
Idiomatic Kotlin Code

Immutability Non-Nullability
val String
var String?

No Static Access
No direct language
feature

8
Test Class Lifecycle
JUnit4: Always New Test Class Instances
class RepositoryTest {

val mongo = startMongoContainer()


Exe d o
e c te
@Test
fun test1() { ... } instance1: RepositoryTest

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

} Where to put the initial setup code? 10


JUnit4: Static for the Initial Setup Code

Bo l la !
class RepositoryTest { nu
companion object { wo r u mu l
@JvmStatic private lateinit var mongo: GenericContainer
s a c @JvmStatic private lateinit var repo: Repository
@BeforeClass @JvmStatic
fun initialize() {
mongo = startMongoContainer()
repo = Repository(mongo.host, mongo.port)
}
}
}

11
JUnit5 to the Rescue!

12
JUnit5: Reuse the Test Class Instance
@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
}
Idi ic
13
JUnit5: Reuse the Test Class Instance
@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)
}
}
14
JUnit5: Change the Lifecycle Default

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

junit.jupiter.testinstance.lifecycle.default = per_class

@TestInstance(TestInstance.Lifecycle.PER_CLASS)

15
Test Names and Grouping
Backticks

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

17
Whi t b o g
to c t o ?

18
@Nested Inner Classes
class DesignControllerTest {
@Nested
inner class GetDesigns {
@Test
fun `all fields are included`() {} getDesign()
@Test
fun `limit parameter`() {}
}
@Nested
inner class DeleteDesign {
@Test
deleteDesign()
fun `design is removed in db`() {}
}
} 19
20
Kotlin Test Libraries
Being Spoilt for Choice

Test Frameworks Mocking Assertions

Kotlin Spek Mockito-Kotlin Strikt Atrium


KotlinTest MockK HamKrest Expekt
Kluent AssertK

Java JUnit5 AssertJ

Incomplete list. My e s c o (fo w)


Some libraries fit into multiple categories. 22
Test-Specific Extension Functions

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))

Dup ti
fun AbstractFloatAssert<*>.isCloseTo(expected: Float)
= this.isCloseTo(expected, Offset.offset(0.001f))

// Usage:
assertThat(taxRate1).isCloseTo(0.3f) Cle
assertThat(taxRate2).isCloseTo(0.2f)
assertThat(taxRate3).isCloseTo(0.5f) Idi ic
23
Mock Handling
Classes Are Final by Default

Solutions

● Interfaces
● open explicitly
● Mockito: Enable incubating feature to
mock final classes
● MockK

25
MockK
mockk(relaxed=true)

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) }

26
MockK
verifySequence {
clientMock.getUser(2)
repoMock.saveUser(user)
}
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)) 27
Does Test Speed Matter?

2 s o 31 Uni T t ?

28
Don't Recreate Mocks

class DesignControllerTest {
private lateinit var repo: DesignRepository
private lateinit var client: DesignClient
private lateinit var controller: DesignController
@BeforeEach
fun init() {
repo = mockk() Ex e s !
client = mockk()
controller = DesignController(repo, client)
}
}
29
Create Mocks Once, Reset Them

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
}
}

30
Create Mocks Once, Reset Them

2.1 s

0.4 s

31
Handle Classes with State
class DesignViewTest {
private val repo: DesignRepository = mockk()
private lateinit var view: DesignView sa f
@BeforeEach
fun init() {
clearMocks(repo)
view = DesignView(repo) re-c e on ir
}
@Test
fun changeButton() {
assertThat(view.button.caption).isEqualTo("Hi")
view.changeButton()
assertThat(view.button.caption).isEqualTo("Guten Tag")
} 32
Spring Integration
All-Open Compiler Plugin
@Configuration
class SpringConfiguration{
@Bean fun objectMapper()
= ObjectMapper().registerKotlinModule()
}

BeanDefinitionParsingException: Configuration problem:


@Configuration class 'SpringConfiguration' may not be final.

<dependency>
<groupId>org.jetbrains.kotlin</groupId> <compilerPlugins>
<artifactId>kotlin-maven-allopen</artifactId> <plugin>spring</plugin>
<version>${kotlin.version}</version> </compilerPlugins>
</dependency> 34
Constructor Injection for Spring-free Testing
@Component
class DesignController(
private val designRepo: DesignRepository,
private val designClient: DesignClient,
) {}

Eas t Log t o Sp g:
val repo: DesignRepository = mockk()
val client: DesignClient = mockk()
val controller = DesignController(repo, client)
35
Utilize Data Classes
Data Classes for Assertions

org.junit.ComparisonFailure: expected:<[2]> but was:<[1]>

???
Expected :2
Actual :1

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

37
Data Classes for Assertions

val expectedDesign = Design(id = 2, userId = 9, name = "Cat")


assertThat(actualDesign).isEqualTo(expectedDesign)

org.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 -ex n o y
38
Data Classes for Assertions
assertThat(actualDesigns).containsExactly(
Design(id = 1, userId = 9, name = "Cat"),
Design(id = 2, userId = 4, name = "Dog")
)
Expecting:
<[Design(id=1, userId=9, name=Cat),
Design(id=2, userId=4, name=Dogggg)]>
to contain exactly (and in same order):
Gre !
<[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)]>
39
Data Classes for Assertions
Single Element
assertThat(actualDesign)
.isEqualToIgnoringGivenFields(expectedDesign,"id")
assertThat(actualDesign)
.isEqualToComparingOnlyGivenFields(expectedDesign,"name")

Lists
assertThat(actualDesigns)
.usingElementComparatorIgnoringFields("id")
.containsExactly(expectedDesign1, expectedDesign2)
assertThat(actualDesigns)
.usingElementComparatorOnFields("name")
.containsExactly(expectedDesign1, expectedDesign2)
40
Helper Function for Object Creation
val testDesign = Design(
id = 1,
userId = 9
name = "Fox",
dateCreated = Instant.now(),
tags = mapOf() - Blo c e
)
val testDesign2 = Design( - Are p s e v
id = 2,
userId = 9
fo h t?
name = "Cat",
dateCreated = Instant.now(),
tags = mapOf()
) 41
Helper Function for Object Creation
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( // Usage:
id = id, val testDesign = createDesign()
userId = 9, val testDesign2 = createDesign(
name = name, id = 1,
dateCreated = date, name = "Fox"
tags = tags ) Con
) 42
Helper Function for Object Creation

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

Ta l e Cr i F n i n
fo C r Tes
43
Helper Function for Object Creation
CurrentTest.kt:
fun createDesign(
isEnabled: Boolean,
language: Locale
) = createDesign( CreationUtils.kt
description = createDescription(
translations = createTranslationsFor(language)
),
state = if (isEnabled) createDisabledState() else
createEnabledState()
)
44
Data Classes for Parameterized Tests

@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 a d?
45
Data Classes for Parameterized Tests

data class TestData(


val input: String?,
val expected: Token?
)

46
Data Classes for Parameterized Tests
@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)
)

47
Conclusion
open 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")
}
49
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"))
}
}

50
Best Practices for Testing in Kotlin
JUnit5 Kotlin Naming, Grouping
@Tes I s a c (PE _C A S) Bac c @Nes

Libraries
Cho or ge
Mock Handling Data Classes FTW
Don't e r e; re ! Equ As e t
Moc Cre o H l
@Par er Tes 52
https://blog.philipphauer.de/best-practices-unit-testing-kotlin/
53
Thank you!

@philipp_hauer
Spreadshirt
KotlinConf, Amsterdam
Oct 05, 2018
Backup Slides
Test-Specific Extension Functions
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.") 56
Spring Integration
Mock-based Spring Test Context

@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() {
@Configuration
clearMocks(repoMock)
private class TestConfig {
}
@Bean
@Test
fun repoMock(): DesignRepository
fun test() {}
= mockk()
}
} 58
Spring Test Context for an Integration Test

@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 va le Sp g
59
About Spreadshirt
Spreadshirt

For two years


61

You might also like