4 - Best Practices For Unit Testing in Kotlin
4 - Best Practices For Unit Testing in Kotlin
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 {
@Test
fun test2() { ... } instance2: RepositoryTest
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
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)
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()
}
<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
???
Expected :2
Actual :1
assertThat(actualDesign.id).isEqualTo(2)
assertThat(actualDesign.userId).isEqualTo(9)
assertThat(actualDesign.name).isEqualTo("Cat")
37
Data Classes for Assertions
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
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)))
// 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