woowa Kotlin
μ½νλ§ λ§€μ° μμ 체νκΈ°
μλ ν ν¬ μΈλ―Έλ λ΄μ©μ μμ½ν λ΄μ©μ λλ€.
Kopring Sample Repository
Kotlin?
μ½νλ¦°μ λ©ν° νλ«νΌ μΈμ΄


μ½λ© 컨벀μ
μ»€λ° λ©μΈμ§
ktlint
μ½λμ 컨벀μ κ·μ½
ktlintλ
Kotlin Coding Conventionκ³ΌAndroid Kotlin Style Guideλ₯Ό κΈ°λ³ΈμΌλ‘ λ°λ₯΄κ³ μλ€.
plugins {
...
id("org.jlleitschuh.gradle.ktlint") version "10.3.0" // Kotlin μ½λ μ€νμΌμ μλμΌλ‘ κ²μ¬νκ³ ν¬λ§·ν
νλ λꡬ
}ktlint: μ½νλ¦° style, convention κ°μ΄λ μ μ©
βοΈ IntelliJ IDEA formatterλ₯Ό ktlintμ λ§κ² μ€μ (ν΄λΉ νλ‘μ νΈλ§):
$ ./gradlew ktlintApplyToIdea
IntelliJ μ¬μ© λͺ¨λ νλ‘μ νΈμ formatter μ μ©(λͺ¨λ IDEA νλ‘μ νΈμ):
$ ./gradlew ktlintApplyToIdeaGlobally
μλμΌλ‘ ktlintλ₯Ό μ΄μ©νμ¬ μ€νμΌ μ²΄ν¬:
$ ./gradlew clean ktlintCheckktlInt Check:
Tasks β verification β ktlintCheck
βοΈ Git hookμ ν΅ν΄ ktlint μ€μ : μ»€λ° μ μ ktlintCheck ν μ€νΈ μ€ν
$ mkdir .git/hooks $ ./gradlew addKtlintCheckGitPreCommitHook
Kotlin/JVM
Basic

μ½νλ¦°μ μμ νκ² μ½λ©νλλ‘ μμνμ§ μμλ μμ νκ² μ½λ©λλλ‘ μ§μ
@NotNull, @Nullable, final μλ μ μ©
μμ νκ² μ½λ©νκ³ μΆμ§ μμ κ²½μ° μμμ μΌλ‘ μ½λ©
μ½νλ¦° μ»΄νμΌ
μ½νλ¦° μ½λλ§ μ¬μ©ν κ²½μ°

μ½νλ¦°, μλ° μ½λλ₯Ό ν¨κ» μ¬μ©ν κ²½μ°
둬볡μμ μμ±λ μλ° μ½λλ μ½νλ¦° μ½λμμ μ κ·Ό λΆκ°

Item 1. μ½νλ¦° νμ€ λΌμ΄λΈλ¬λ¦¬
μ½νλ¦° νμ€ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ΅νκ³ μ¬μ©νκΈ°
μ½νλ¦°μμ μλ°μ κ΄λ ¨λ importλ₯Ό μ΅λν μ κ±°νλ €κ³ λ Έλ ₯νμ
νμ€ λΌμ΄λΈλ¬λ¦¬λ₯Ό μ¬μ©νλ©΄ κ·Έ μ½λλ₯Ό μμ±ν μ λ¬Έκ°μ μ§μκ³Ό μ¬λ¬λΆλ³΄λ€ μμ μ¬μ©ν λ€λ₯Έ νλ‘κ·Έλλ¨Έλ€μ κ²½νμ νμ©ν μ μλ€.
AS-IS
import java.util.Random import java.util.concurrent.ThreadLocalRandom Random.nextInt() ThreadLocalRandom.current().nextInt()TO-BE
import kotlin.random.Random Random.nextInt() // thread safe
μ½νλ¦°μ μ½κΈ° μ μ© μ»¬λ μ κ³Ό λ³κ²½ κ°λ₯ν 컬λ μ μ ꡬλ³ν΄ μ 곡
μΈν°νμ΄μ€λ₯Ό λ§μ‘±νλ μ€μ 컬λ μ μ΄ λ°ν
λ°λΌμ νλ«νΌλ³ 컬λ μ μ μ¬μ© κ°λ₯
Item 2. μλ°λ‘ μμ»΄νμΌ
μλ°λ‘ μμ»΄νμΌνλ μ΅κ΄ λ€μ΄κΈ°
μ½νλ¦° μλ ¨λλ₯Ό λμ΄λ κ°μ₯ μ’μ λ°©λ²μ μ½νλ¦°μΌλ‘ μμ±ν μ½λκ° μλ°λ‘ μ΄λ»κ² ννλλμ§ νμΈνκΈ°
μμ»΄νμΌλ‘ μκΈ°μΉ μμ μ½λ μμ±μ λ°©μ§
κΈ°μ‘΄ μλ° λΌμ΄λΈλ¬λ¦¬μ νλ μμν¬λ₯Ό μ¬μ©νλ©° λ¬Έμ κ° λ°μν λ λΉ λ₯΄κ² νμΈ κ°λ₯
Kotlin to Java
Tool > Kotlin > Show Kotlin Bytecode > Decompile
Java to Kotlin
Convert Java File to Kotlin File
Item 3. 둬볡λμ λ°μ΄ν° ν΄λμ€
둬볡 λμ λ°μ΄ν° ν΄λμ€ μ¬μ©νκΈ°
λ°μ΄ν° ν΄λμ€λ₯Ό μ¬μ©νλ©΄ μ»΄νμΌλ¬κ°
equals(),hashCode(),toString(),copy()λ±μ μλ μμ±μ£Ό μμ±μμ λ§€κ°λ³μλ₯Ό κΈ°μ€μΌλ‘ μμ±νκ³ , μ£Ό μμ±μμλ νλ μ΄μμ λ§€κ°λ³μκ° μμ΄μΌ ν¨
λ§€κ°λ³μλ val λλ varλ‘ νμ
λ°μ΄ν° ν΄λμ€μ property λ₯Ό μ μΈνλ μκ° ν΄λΉ property λ field, Getter, Setter, μμ±μ νλΌλ―Έν° μν
val person = Person(name = "Aaron", age = 29) val person2 = person.copy(age = 25) data class Person(val name: String, val age: Int)


plugins
plugins {
val kotlinVersion = "1.6.21"
id("org.springframework.boot") version "2.3.3.RELEASE" // μ€νλ§ λΆνΈλ₯Ό μ¬μ©νκΈ° μν νλ¬κ·ΈμΈ
id("io.spring.dependency-management") version "1.0.10.RELEASE" // μ€νλ§ λΆνΈ νλ‘μ νΈμμ μμ‘΄μ± κ΄λ¦¬λ₯Ό μ½κ² ν μ μλλ‘ μ§μνλ νλ¬κ·ΈμΈ
kotlin("jvm") version kotlinVersion // Kotlin JVMμ μ¬μ©νλ νλ‘μ νΈλ₯Ό μν νλ¬κ·ΈμΈ
kotlin("plugin.spring") version kotlinVersion
kotlin("plugin.jpa") version kotlinVersion
id("org.jlleitschuh.gradle.ktlint") version "10.3.0" // Kotlin μ½λ μ€νμΌμ μλμΌλ‘ κ²μ¬νκ³ ν¬λ§·ν
νλ λꡬ
id("org.asciidoctor.jvm.convert") version "3.3.2" // Asciidoctorλ₯Ό μ¬μ©ν΄μ λ¬Έμλ₯Ό μμ±νκ³ λ³νν μ μλλ‘ μ§μνλ νλ¬κ·ΈμΈ
id("org.flywaydb.flyway") version "7.12.0" // Flywayλ₯Ό μ¬μ©νμ¬ λ°μ΄ν°λ² μ΄μ€ λ§μ΄κ·Έλ μ΄μ
μ μλμΌλ‘ κ΄λ¦¬ν μ μκ² ν΄μ£Όλ νλ¬κ·ΈμΈ
}Spring Boot
final ν΄λμ€
@SpringBootApplicationμ@Configurationμ ν¬ν¨νκ³ , μ€νλ§μ κΈ°λ³Έμ μΌλ‘ CGLIBμ μ¬μ©ν΄μ@Configurationν΄λμ€μ λν νλ‘μλ₯Ό μμ±νμ§λ§, final ν΄λμ€λ μμμ΄λ μ€λ²λΌμ΄λκ° λΆκ°νλ―λ‘ νλ‘μ μμ±μ΄ λΆκ°
μμμ νμ©νκ³ μ€λ²λΌμ΄λλ₯Ό νμ©νλ €λ©΄
openλ³κ²½μ μΆκ° νμSpring Framework 5.2 λΆν°λ
@Configurationμ proxyBeanMethod μ΅μ μ ν΅ν΄ νλ‘μ μμ± λΉνμ±ν κ°λ₯
All-open μ»΄νμΌλ¬ νλ¬κ·ΈμΈ
μ½νλ¦°μ λ€μν μ»΄νμΌλ¬ νλ¬κ·ΈμΈμ μ 곡
all-openμ»΄νμΌλ¬ νλ¬κ·ΈμΈμ μ§μ ν μ λν μ΄μ μ΄ μλ ν΄λμ€μ λͺ¨λ λ©€λ²μ open λ³κ²½μλ₯Ό μΆκ°μ€νλ§μ μ¬μ©ν κ²½μ°
all-openμ»΄νμΌλ¬ νλ¬κ·ΈμΈμ λννkotlin-springμ»΄νμΌλ¬ νλ¬κ·ΈμΈμ μ¬μ© κ°λ₯@Component,@Transactional,@Asyncλ±μ΄ κΈ°λ³Έμ μΌλ‘ μ§μ
File > Project Structure > Project Settings > Modules > Kotlin > Compiler Plugins μμ μ§μ λ μ λν μ΄μ νμΈ κ°λ₯
plugins {
kotlin('plugins.spring') version "1.5.21"
}
allOpen {
annotation("com.my.Annotation")
}Item 4. μ§μ° μ΄κΈ°ν
νλ μ£Όμ μ΄ νμνλ©΄ μ§μ° μ΄κΈ°νλ₯Ό μ¬μ©νμ
μ½νλ¦°μμ
lateinitλ³κ²½μλ₯Ό λΆμ΄λ©΄ νλ‘νΌν°λ₯Ό λμ€μ μ΄κΈ°νν μ μλ€. λμ€μ μ΄κΈ°ννλ νλ‘νΌν°λ νμ var
@Autowired
private lateinit var objectMapper: ObjectMapperjackson-module-kotlin
jacksonμ κΈ°λ³Έμ μΌλ‘ μμ§λ ¬ν κ³Όμ μ μν΄ λ§€κ°λ³μκ° μλ μμ±μκ° νμνμ§λ§
μ½νλ¦°μμ λ§€κ°λ³μκ° μλ μμ±μλ₯Ό λ§λ€κΈ° μν΄ λͺ¨λ λ§€κ°λ³μμ κΈ°λ³Έ μΈμκ° νμ
jackson-module-kotlinμ λ§€κ°λ³μκ° μλ μμ±μκ° μλλΌλ μ§λ ¬νμ μμ§λ ¬νλ₯Ό μ§μμ½νλ¦°μ λ§€κ°λ³μκ° μλ μμ±μλ₯Ό λ§λ€κΈ° μν΄ μμ±μμ λͺ¨λ λ§€κ°λ³μμ κΈ°λ³Έ μΈμκ° νμ
// Unit Testμ κ²½μ° ObjectMapper μ§μ νΈμΆ νμ val mapper1 = jacksonObjectMapper() val mapper2 = ObjectMapper().registerKotlinModule()dependencies { implementation("com.fasterxml.jackson.module:jackson-module-kotlin") }
Kotlin Annotation

λ°μ΄ν° ν΄λμ€μ propertyλ₯Ό μ μΈνλ μκ° field, Getter, Setter, μμ±μ νλΌλ―Έν° μν μν
@param.JsonProperty: parameterμ μ¬μ©λ μ΄λ¦@get.JsonProperty: getterμ μ¬μ©λ μ΄λ¦
Item 5. λ³κ²½ κ°λ₯μ± μ ν
λ³κ²½ κ°λ₯μ±μ μ ννμ
μ½νλ¦° ν΄λμ€μ λ©€λ²κ° finalμΈ κ²μ²λΌ μΌλ¨ valλ‘ μ μΈνκ³ νμ μ varλ‘ λ³κ²½νμ.
μμ±μ λ°μΈλ©μ μ¬μ©νλ €λ©΄
@EnableConfigurationPropertiesλλ
@ConfigurationPropertiesScanμ¬μ©
@ConfigurationProperties("application") @ConstructorBinding data class ApplicationProperties(val url: String) @ConfigurationPropertiesScan @SpringBootApplication class Applicationprivate property and backing property
κ³΅κ° API μ ꡬν μΈλΆ μ¬ν νλ‘νΌν°λ‘ λλ κ²½μ°
private νλ‘νΌν° μ΄λ¦μ μ λμ¬λ‘ λ°μ€μ μ¬μ©(backing property)
JVMμμλ κΈ°λ³Έ getter, setterκ° μλ private νλ‘νΌν°μ λν΄ ν¨μ νΈμΆ μ€λ²ν€λλ₯Ό λ°©μ§νλλ‘ μ΅μ ν
@OneToMany(cascade = [CascadeType.PERSIST, CascadeType.MERGE], orphanRemoval = true) @JoinColumn(name = "session_id", nullable = false) private val _students: MutableSet<Student> = students.toMutableSet() //backing property val student: Set<Student> // public get() = _students
Persistence
No-arg μ»΄νμΌλ¬ νλ¬κ·ΈμΈ
JPAμμ μν°ν° ν΄λμ€λ₯Ό μμ±νλ €λ©΄ λ§€κ°λ³μκ° μλ μμ±μκ° νμ
no-argμ»΄νμΌλ¬ νλ¬κ·ΈμΈμ μ§μ ν μ λν μ΄μ μ΄ μλ ν΄λμ€μ λ§€κ°λ³μκ° μλ μμ±μλ₯Ό μΆκ°JPA, Kotlinμμ μ§μ νΈμΆν μ μμ§λ§ 리νλμ μ μ¬μ©νμ¬ νΈμΆ κ°λ₯
kotlin-spring Compiler Pluginκ³Ό λ§μ°¬κ°μ§λ‘ JPAλ₯Ό μ¬μ©νλ κ²½μ° no-arg μ»΄νμΌλ¬ νλ¬κ·ΈμΈμ λννkotlin-jpa Compiler Pluginμ¬μ© κ°λ₯JPA, Kotlin μ¬μ© μ μλμΌλ‘
kotlin-jpa Compiler PluginμΆκ°@Entity,@Embeddable,@MappedSuperclassκ° κΈ°λ³Έμ μΌλ‘ μ§μ
plugins {
kotlin('plugins.spring') version "1.5.21"
kotlin('plugins.jpa') version "1.5.21"
}
allOpen { // JPA μ§μ°λ‘λ© μ μ©μ μν΄ νλ‘μ μμ± λͺ©μ μΌλ‘ μ€μ
annotation("javax.persistence.Entity")
annotation("javax.persistence.MappedSuperclass")
}Item 6. μν°ν°
μν°ν°μ λ°μ΄ν° ν΄λμ€ μ¬μ©μ νΌνμ.
μλ°©ν₯ μ°κ΄ κ΄κ³μ κ²½μ°
toString(),hashcode()λ‘ λ¬΄ν μν μ°Έμ‘°κ° λ°μ
Item 7. μ¬μ©μ μ§μ getter
μ¬μ©μ μ§μ getterλ₯Ό μ¬μ©νμ.
JPA μ μν΄ μΈμ€ν΄μ€ν λ λ, μ΄κΈ°ν λΈλ‘μ΄ νΈμΆλμ§ μμΌλ―λ‘, μμννμ§ μλ νλλ μ¬μ©μ μ§μ getterλ₯Ό μ¬μ©νμ.
/**
* AS-IS: μ΄κΈ°νλ νλ‘νΌν°
* μμννμ§ μμ λͺ©μ μ΄μμ§λ§, JPA μ μν΄ μΈμ€ν΄μ€ν λ λ
* nullμ νμ©νμ§ μμμμλ λΆκ΅¬νκ³ , μ΄κΈ°ν λΈλ‘μ΄ νΈμΆλμ§ μμΌλ―λ‘ nullμ΄ λ€μ΄κ°κ² λλ€.
*/
@Transient
val fixed: Boolean = startDate.until(endDate).year < 1
---
/**
* TO-BE: μ¬μ©μ μ§μ getter
* λ°λΌμ, μ¬μ©μ μ§μ getter λ₯Ό μ μν΄μ νλ‘νΌν°μ μ κ·Όν λλ§λ€ νΈμΆλλλ‘ νμ.
* λ·λ°μΉ¨νλ νλ(backing property)κ° μ‘΄μ¬νμ§ μμΌλ―λ‘ AccessType.FIELD λΌλ @Transient λΆνμ
*/
val fixed: Boolean
get() = startDate.until(endDate).year < 1 // μΌμ’
μ λ©μλλ‘ μκ°νμItem 8. Null νμ
μ κ±°
Nullμ΄ λ μ μλ νμ μ λΉ λ₯΄κ² μ κ±°νμ.
Nullμ΄ λ μ μλ νμ μ μ¬μ©νλ©΄ Null κ²μ¬λ !! μ°μ°μκ° νμνλ€.
μν°ν° ν΄λμ€μ idλ₯Ό 0 λλ λΉ λ¬Έμμ΄λ‘ μ΄κΈ°νν΄μ Nullμ΄ λ μ μλ νμ μ μ κ±°νμ.
Optional λ³΄λ€ Nullable ν νμ μ μ¬μ©ν΄μ λΆνμν java import λ₯Ό μ€μ΄μ.
interface ArticleRepository : CrudRepository<Article, Long> { fun findBySlug(slug: String): Article? // nullable Article fun findAllByOrderByAddedAtDesc(): Iterable<Atricle> } interface UserRepository : CrudRepository<User, Long> { fun findByLogin(login: String): User? }νμ₯ ν¨μλ₯Ό μ¬μ©ν΄ λ°λ³΅λλ Null κ²μ¬λ₯Ό μ κ±°ν μ μλ€.
fun MemberRepository.getById(id: Long): Member { if (id == 0L) { return Member.SINGLE } return findByIdOrNull(id) ?: throw NoSuchElementException("μ‘΄μ¬νμ§ μλ μμ΄λ μ λλ€. id: $id") } interface MemberRepository : JpaRepository<Member, Long>
Test
λ¨μ ν
μ€νΈ
Junit, Mockito κ°μ Java λΌμ΄λΈλ¬λ¦¬λ‘ ν μ€νΈνλ―λ‘ Kotlin λ€μ΄ μ½λ μμ±μ΄ μ΄λ €μ
μμΈ ν μ€νΈλ JUnit5μ
assertThrowsλ°assertDoesNotThrowκ°μ Kotlin ν¨μλ₯Ό μ¬μ©νλ©΄ κ°κ²°
// Java μ€νμΌ
@DisplayName("νμμ λΉλ°λ²νΈμ λ€λ₯Ό κ²½μ° μμΈκ° λ°μνλ€")
@Test
fun authenticate {
val user = createUser
assertThatExceptionOfType(UnidentifiedUserException::class.java)
.isThrownBy { user.authenticate(WRONG_PASSWORD) }
}
// Kotlin μ€νμΌ
@Test
fun `νμμ λΉλ°λ²νΈμ λ€λ₯Ό κ²½μ° μμΈκ° λ°μνλ€`() {
val user = createUser
assertThrows<UnidentifiedUserException> {
user.authenticate(WRONG_PASSWORD)
}
}
---
// Kotest
"νμμ λΉλ°λ²νΈμ λ€λ₯Ό κ²½μ° μμΈκ° λ°μνλ€" {
val user = createUser()
shouldThrow<UnidentifiedUserException> { user.authenticate(WRONG_PASSWORD) }
}ν
μ€νΈ ν©ν 리
ν μ€νΈ ν½μ€μ²λ₯Ό λ°ννλ
ν©ν 리 ν¨μλ₯Ό λ§λ€ μ μλ€.ν μ€νΈ ν½μ€μ²: ν μ€νΈλ₯Ό μν μ μ 쑰건
Kotlin κΈ°λ³Έ μΈμλ₯Ό μ¬μ©νλ©΄
λΉλ ν¨ν΄μ²λΌ λ€μν κ²½μ°λ₯Ό μ²λ¦¬ κ°λ₯κΈ°λ³Έ μΈμμμ΄λ¦ λΆμΈ μΈμλ₯Ό μ μ ν μ¬μ©νλ©΄ ν μ€νΈνλ €λ κ΄μ¬μ¬λ₯Ό λλ¬λ΄λ λ° μ¬μ© κ°λ₯
createMission(submittable = true) // κ³Όμ μ μΆλ¬Όμ μ μΆν μ μλ κ³Όμ
createMission(submittable = false) // κ³Όμ μ μΆλ¬Όμ μ μΆν μ μλ κ³Όμ
createMission(title = "κ³Όμ 1", evaluationId = 1L) // νΉμ νκ°μ λν κ³Όμ
createMission(hidden = false) // μ¨κΈ°μ§ μμ κ³Όμ
// κ΄λ ¨ μλ μΈμμλ ν©λ¦¬μ μΈ κΈ°λ³Έκ°μ μ¬μ©
fun createMission(
title: String = MISSION_TITLE,
description: String = MISSION_DESCRIPTION,
evaluationId: Long = 1L,
startDateTime: LocalDateTime = START_DATE_TIME,
endDateTime: LocalDateTime = END_DATE_TIME,
submittable: Boolean = true,
hidden: Boolean = false,
id: Long = 0L
): Mission {
return Mission(title, description, evaluationId, startDateTime, endDateTime, submittable, hidden, id)
}ν
μ€νΈ νμ₯ ν¨μ
νμ₯ν¨μλ₯Ό μ¬μ©νμ¬ κ°μ λ μ½κ² νν
private val Int.ms: Long get() = milliseconds.inWholeMilliseconds
"μ΄λΉ 1νλ‘ μ νν κ²½μ° 1μ΄ κ°κ²© μμ²μ μ±κ³΅νλ€" {
val limiter = RateLimiter(1)
listOf(0.ms, 1000.ms, 2000.ms).forAll {
shouldNotThrowAny { limiter.acquire(it) }
}
}
"μ΄λΉ 1νλ‘ μ νν κ²½μ° μ΄λΉ 2λ² μμ²νλ©΄ μ€ν¨νλ€" {
val limiter = RateLimiter(1).apply {
acquire(0.ms)
}
listOf(100.ms, 500.ms, 900.ms).forAll {
shouldThrow<ExceededRequestException> { limiter.acquire(it) }
}
}νμ₯ν¨μλ‘ κ²μ¦νκ³ μ νλ λΆλΆμ κ°λ μ± ν₯μ
private fun ApplicantAndFormResponse.hasKeywordInNameOrEmail(keyword: String): Boolean {
return name.contains(keyword) || email.contains(keyword)
}
...
When("νΉμ ν€μλλ‘ νΉμ λͺ¨μ§μ μ§μν μ§μ μ 보λ₯Ό μ‘°ννλ©΄") {
val actual = applicantService.findAllByRecruitmentIdAndKeyword(recruitmentId, keyword)
Then("μ§μ μ 보 λ° λΆμ νμ μ¬λΆλ₯Ό νμΈν μ μλ€") {
actual shouldHaveSize 2
actual.filter { it.hasKeywordInNameOrEmail(keyword) } shouldHaveSize 2 // νμ₯ν¨μ μ μ©
actual[0].isCheater.shouldBeTrue()
actual[1].isCheater.shouldBeFalse()
}
}Kotest μ΄μ€μ
ππ» ν μ€νΈ μ΄μ€μ
μΌλΆ μ΄μ€μ μ΄ μ€ν¨νλλΌλ ν μ€νΈκ° μ¦μ μ€μ§λμ§ μκ³ , λκΉμ§ κ²μ¦
μ΄λ€ νλ‘νΌν° λλ¬Έμ μ€ν¨νλμ§ λ°λ‘ μμμ±κΈ° μ΄λ €μ
val excepted = Speaker("Aaron", "Park", 30)
val actual = Speaker("Aaron", "Kim", 28)
assertAll (
{ assertThal(actual.firstName).isEqualTo(expexted.firstName) },
{ assertThal(actual.lastName).isEqualTo(expexted.lastName) },
{ assertThal(actual.age).isEqualTo(expexted.age) },
)μΈμ€ν΄μ€λ₯Ό λ°μ΄ν° ν΄λμ€λ‘ λ§λ€κ³ λΉκ΅νλ κ²μ κΆμ₯
val excepted = Speaker("Aaron", "Park", 30)
val actual = Speaker("Aaron", "Kim", 28)
assertThal(actual).isEqualTo(expexted)μ£Όλ‘ JUnitμ Assertions λμ μ½κΈ° μ¬μ΄ AssertJμ Assertionsλ₯Ό μ¬μ©
ππΌ Kotest μ΄μ€μ
Kotest μ΄μ€μ μ κ°κ²°νκ³ κΈ°μ‘΄ JUnit5μ νΌμ© κ°λ₯
μ€λ§νΈ μΊμ€νΈ κ°μ Kotlin κΈ°λ₯μ μ§μ
ν λ² μ€λ§νΈ μΊμ€ν λμλ€λ©΄ nullμ λ€λ£¨μ§ μμλ λλ€.
val actual = userRepository.findByEmail("aaron@google.com")
actual.sholudNotBeNull() // μ€λ§νΈ μΊμ€νΈ
actual.name shouldBe "λ°μ§ν"Kotlinμμ Kotestκ° κ°μ₯ λ§μ΄ μ¬μ©
λ€μν μ€νμΌμ ν μ€νΈ λ μ΄μμμ μ 곡
κ·Έ μ€μμλ StringSpecμ μ¬μ©νλ©΄ μ λν μ΄μ μ μ¬μ©νμ§ μμλ λλ€.
// JUnit 5
class AuthenticationCodeTest {
@Test
fun `μΈμ¦ μ½λλ₯Ό μμ±νλ€`() {
val authenticationCode = AuthenticationCode(EMAIL)
assertAll (
{ assertThal(actual.firstName).isEqualTo(expexted.firstName) },
{ assertThal(actual.lastName).isEqualTo(expexted.lastName) },
{ assertThal(actual.age).isEqualTo(expexted.age) },
)
}
...
}
---
// Kotest
class AuthenticationCodeTest : StringSpec({
"μΈμ¦ μ½λλ₯Ό μμ±νλ€" {
val authenticationCode = AuthenticationCode(EMAIL)
assertSoftly(authenticationCode) {
code.shouldNotBeNull()
authenticated.shouldBeFalse()
createdDateTime.shouldNotBeNull()
}
}
"μ½λλ₯Ό μΈμ¦νλ€" {
val authenticationCode = AuthenticationCode(EMAIL, VALID_CODE)
authenticationCode.authenticate(VALID_CODE)
authenticationCode.authenticated.shouldBeTrue()
}
...
})λͺ¨μ ν
μ€νΈ
MockK
ππ» Mockito
Mockito final ν΄λμ€μ final λ©μλλ λͺ¨μ λΆκ°
Kotlin νμ₯ ν¨μλ Javaμ μ μ λ©μλμ΄λ©° Mockitoλ μ΄λ₯Ό μ€ν λΆκ°
μ΅μμ ν¨μλ νΉμ ν΄λμ€μ μνμ§ μκΈ° λλ¬Έμ μ€ν λΆκ°
ππΌ MockK
DSL κΈ°λ° Kotlin λͺ¨μ λΌμ΄λΈλ¬λ¦¬
μ½λ κΈ°λ°, μ λν μ΄μ κΈ°λ° λ± λλΆλΆ Mockitoμ λμΌ
val recruitmentRepository = mockk<RecruitmentRepository>()
val recruitmentItemRepository = mockk<RecruitmentItemRepository>()
every { recruitmentRepository.save(any()) } returns recruitment
every { recruitmentItemRepository.findByRecruitmentIdOrderByPosition(any()) } returns emptyList()
every { recruitmentItemRepository.deleteAll(any()) } just Runs
slot<MethodParameter>().also { slot ->
every { it.supportsParameter(capture(slot)) } answers {
slot.captured.hasParameterAnnotation(LoginUser::class.java)
}
}β MockKλ₯Ό μ΄μ©ν μ°μ μ€ν°λΉ
MockKλ‘ μ μ ν¨μλ₯Ό λͺ¨μνμ§ μκ³ νμ₯ ν¨μλ₯Ό μ€ν νμ¬ λ΄λΆμ λ©€λ² ν¨μλ₯Ό μ€ν νλ λ°©λ²
λκ΅¬κ° κΈ°λ₯μ μ 곡νλ€κ³ ν΄μ νμ μ¬μ©ν΄μΌ νλ κ²μ μλλ€
κ°μ§ κ°μ²΄λ₯Ό μ¬μ©νλ κ²λ μ’μ λ°©λ²
every { recruitmentRepository.getById(any()) } returns createRecruitment(id = recruitmentId)
fun RecruitmentRepository.getById(id: Long): Recruitment = findByIdOrNull(id)
?: throw NoSuchElementException("λͺ¨μ§μ΄ μ‘΄μ¬νμ§ μμ΅λλ€. id: $id")
// CrudRepositoryExtensions
fun <T, ID> CrudRepository<T, ID>.findByIdOrNull(id: ID): T? =
findById(id).orElse(null)ππ» Junit 5 + Mockito

ππΌ Kotest + MockK

Kotestλ₯Ό μ΄μ©ν λͺ¨μ ν
μ€νΈ
BDDλ₯Ό μ§μνλ BehaviorSpec, DescribeSpecμ΄ μ‘΄μ¬
ν μ€νΈκ° νλμ λ¬Έμλ‘μ λμ± νλΆ
μ€μ²© ν μ€νΈλ ν μ€νΈ μλͺ μ£ΌκΈ°λ₯Ό μ΄ν΄νλ κ²μ΄ μ€μ
Kotest μλͺ μ£ΌκΈ°
Given, When, Then μ μ΄λ£¨λ λͺ¨λ κ²μ beforeSpec, afterSpecμΌλ‘ listen
Given, When, Then λ§μ λ³Έλ€λ©΄ Container λΌκ³ λΆλ₯΄κ³
beforeContainer, afterContainer λ©μλλ‘ μλͺ μ£ΌκΈ° κ΄λ¦¬ κ°λ₯
Then μ beforeEach, afterEach λ©μλλ‘ μλͺ μ£ΌκΈ° κ΄λ¦¬ κ°λ₯
Given("νΉμ λͺ¨μ§μ λν μμ μ§μμλ₯Ό μμ±ν μ§μμκ° μκ³ λͺ¨μ§ νλͺ©μ΄ μλ κ²½μ°") {
...
When("λ―Έμ μΆ νλͺ©μ΄ μλ μνμμ μ§μμλ₯Ό μ΅μ’
μ μΆνλ©΄") {
..
Then("μμΈκ° λ°μνλ€") {
...
}
}
When("νλͺ©μ΄ λΉμ΄ μλ μνμμ μ§μμλ₯Ό μ΅μ’
μ μΆνλ©΄") {
...
Then("μμΈκ° λ°μνλ€") {
...
}
}
}
Given("νΉμ νμμ΄ νΉμ λͺ¨μ§μ λν΄ μμ±ν μμ μ§μμκ° μλ κ²½μ°") {
...
When("ν΄λΉ μ§μμλ₯Ό μ‘°ννλ©΄") {
...
Then("μ§μμλ₯Ό νμΈν μ μλ€") {
...
}
}
}Kotestμ 격리 λͺ¨λ
Kotestλ SingleInstance, InstancePerLeaf, InstancePerTest μΈ κ°μ§ κ°μ΄ μ‘΄μ¬
κΈ°λ³Έμ
SingleInstanceμ΄λ©° ν μ€νΈ μν©μ λ§κ² 격리 λͺ¨λλ₯Ό μ νex. ν μ€νΈ ν΄λμ€ μ 체μ λͺ¨μ κ°μ²΄λ₯Ό λ§λλ λ° λΉμ©μ΄ λ§μ΄ λ λ€λ©΄
SingleInstanceλ₯Ό μ ννκ³ , clearMocks()μ νΈμΆνλ κ²μ΄ λ λμ μ μλ€.
ν΅ν© ν
μ€νΈ
Spring 5.2λΆν° @TestConstructor μ¬μ© μ μμ±μλ₯Ό ν΅ν μ£Όμ
μ΄ κ°λ₯
Kotestλ Spring ν΅ν© ν μ€νΈλ₯Ό μ§μνκΈ° μν΄
SpringExtenstionμ μ 곡λ³λμ μ λν μ΄μ μμ΄ μμ±μ μ£Όμ μ΄ κ°λ₯νλ©°,
SpringExtenstionμ ν΅ν΄μ νΈλμμ λ‘€λ°±λ κ°λ₯
μΈμ ν
μ€νΈ
μ¬μ©μ κ΄μ μμ κΈ°λ₯μ΄ μ¬λ°λ₯΄κ² μλνλμ§ νμΈνκΈ° μν ν μ€νΈ
μΌλ°μ μΌλ‘ μ¬μ©μ μ€ν 리μ λ°λΌ Given-When-Then μ€νμΌλ‘ μμ±
μ°κ΄ κ΄κ³κ° 볡μ‘ν μλ‘ ν μ€νΈ ν½μ€μ² μμ±μ΄ μ΄λ €μμ§
μΈμ ν μ€νΈλ₯Ό μν DSL
μ°κ΄ κ΄κ³λ₯Ό λλ©μΈμ νΉνλ μΈμ΄λ‘ νννκ³ , νμν λ°μ΄ν°κ° μμ±λλλ‘ ν μ μλ€.
μ½λλ₯Ό μ²μ μ νλ μ¬λλ€μ λλ©μΈ νμ΅μ λ§μ λμμ΄ λλ€.

λΆλ‘
Kover
IntelliJ, JaCoCo μμ΄μ νΈλ₯Ό μ¬μ©νλ Kotlin μ½λ 컀λ²λ¦¬μ§ λꡬ
Kotlinμ΄ μμ±ν λ°μ΄νΈμ½λλ‘ μΈ‘μ νλ©° μΈλΌμΈ ν¨μμ κ°μ΄ JaCoCoλ‘ μΈ‘μ ν μ μλ μμλ μΈ‘μ
Last updated