안녕하세요.
Unit Test 프레임워크 중 하나인 JUnit5에 대해 알아보겠습니다.
JUnit5
JVM 기반 언어에서 단위 테스트를 작성하고 실행하는데 사용되는 프레임워크입니다.
그래서 Java, Kotlin 언어로 개발할 때 테스트 프레임워크로 가장 많이 사용됩니다.
JUnit5 in Android Studio
JUnit5를 안드로이드 스튜디오에서 사용하는 방법에 대해 알아보겠습니다,
언어는 Kotlin을 사용할 것입니다.
(버전은 다를 수 있으니 체크해주세요)
build.gradle (app 수준)
android {
...
testOptions {
unitTests.includeAndroidResources = true
}
...
}
dependencies {
// JUnit 5 의존성 추가
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2'
// AndroidJUnit5 통합 의존성 추가
testImplementation 'de.mannodermaus.junit5:android-test-core:1.2.2'
testRuntimeOnly 'de.mannodermaus.junit5:android-test-runner:1.2.2'
}
tasks.withType(Test) {
useJUnitPlatform()
}
Test 만들기
간단한 test를 통해 JUnit5가 제대로 동작하는지 확인해봅시다
먼저, 간단한 계산기 클래스를 만들어 보겠습니다.
경로를 살펴보면 com.example.kotlinpractice.junit에 Calculator 입니다.
package com.example.kotlinpractice.junit
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
}
해당 클래스에서 Alt + Insert를 누르면 Test를 쉽게 만들 수 있습니다.
위에서 봤던 경로와 똑같은 test 폴더 상에 CalculatorTest.kt가 만들어졌습니다.
import로 org.junit.jupiter.api를 가져오는데 성공한다면 문제 없이 잘 돌아가는 것입니다.
package com.example.kotlinpractice.junit
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
class CalculatorTest {
@BeforeEach
fun setUp() {
}
@AfterEach
fun tearDown() {
}
@Test
fun add() {
val calculator = Calculator()
assertEquals(2, calculator.add(1, 1))
}
}
@Test 어노테이션이 붙은 함수를 독립적으로 돌릴 수 있고,
클래스 안의 모든 @Test 함수를 한 번에 돌릴 수도 있습니다.
테스트가 성공적으로 수행되었습니다.
기본 어노테이션 종류
자주 쓰이는 어노테이션 위주로 정리를 해보면 아래와 같습니다.
@Test
Test 메서드 임을 선언함
@DisplayName
클래스나 메소드 위에 붙여서 이름을 지을 수 있음
@BeforeEach
테스트 실행 전에 수행할 메서드를 나타냄
@AfterEach
테스트 실행 후에 수행할 메서드를 나타냄
@Disabled
테스트를 수행하지 않음을 나타냄
아래처럼 클래스를 만들고 모든 경우의 수를 커버 가능한 테스트를 만들어 봅시다.
package com.example.kotlinpractice.junit
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
fun multiply(a: Int, b: Int): Int {
return a * b
}
fun divide(a: Int, b: Int): Int {
if (b == 0) {
throw IllegalArgumentException("Division by zero is not allowed")
}
return a / b
}
}
package com.example.kotlinpractice.junit
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.*
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
calculator = Calculator()
}
@AfterEach
fun tearDown() {
}
@Test
fun `test add`() {
assertEquals(2, calculator.add(1, 1))
}
@Test
fun `test multiply`() {
assertEquals(6, calculator.multiply(2, 3))
}
@Test
fun `test divide`() {
assertEquals(2, calculator.divide(6, 3))
}
@Test
fun `test divideByZero`() {
val exception = assertThrows<IllegalArgumentException> {
calculator.divide(1, 0)
}
assertEquals("Division by zero is not allowed", exception.message)
}
}
이처럼 Unit Test Code는 기존 클래스 대비 길어질 수 밖에 없습니다.
그럼에도 모든 경우의 수를 다 처리해줘야 할까요?
- 최대한 100%를 채워주면 좋긴 하지만,
- 팀에서 요구하는 수치는 그때그떄 다르긴 합니다.
- 제가 다니는 회사는 80% 정도 이상을 지향한다고 하긴 합니다.
Coverage
coverage를 통해 내가 어디까지 테스트를 진행했는지 확인할 수 있습니다.
실행 버튼에 run with coverage를 누르면 확인할 수 있습니다.
CalculatorTest.kt에서
`test divideByZero` 메서드를 없애고 커버리지를 돌려봅시다.
코드를 실제로 없애지 않고, @Disabled 어노테이션을 붙여주면 됩니다.
@Test
@Disabled
fun `test divideByZero`() {
val exception = assertThrows<IllegalArgumentException> {
calculator.divide(1, 0)
}
assertEquals("Division by zero is not allowed", exception.message)
}
test 된 곳은 초록색, 안 된 곳은 빨간 색으로 뜹니다.
다시 추가해서 넣어보면 모든 부분이 커버된 것을 확인할 수 있습니다.
+) 한 테스트 메서드에서 여러 case를 돌리고 싶을 때?
테스트 메서드 안에서 assertEquals를 여러 개 사용해도 되지만, 이는 Unit Test에서 지양하는 편입니다.
Unit Test는 테스트 범위가 작을 수록 적기 때문에 assert 문을 최대한 적게 쓰는 것이 좋습니다.
그럴 때, Parameterized Test를 사용하면 됩니다.
@ParameterizedTest
@CsvSource(
"1, 1, 2",
"2, 3, 5",
"0, 5, 5",
"7, 3, 10"
)
fun add(a: Int, b: Int, expected: Int) {
assertEquals(expected, calculator.add(a, b))
}
이에 대한 더 자세한 내용은 다음 포스팅에서 다루도록 하겠습니다.
+) 의존성이 생기면 어떡하지?
클래스끼리 의존성이 존재하는 부분도 테스트를 해봐야 합니다.
아래 Server 클래스는 User 클래스에 의존성이 있습니다.
class Server {
fun getGrade(user: User): String {
return when {
user.getScore() >= 90 -> "A"
user.getScore() >= 80 -> "B"
user.getScore() >= 70 -> "C"
else -> "D"
}
}
}
이럴 때 Unit Test를 어떻게 하지?
user를 직접 호출해서 테스트할 수 있습니다.
val user = User(name, score)
but) 그 클래스가 무거운 클래스일 수 있거나,
해당 클래스에서 또 의존성이 있으면 연쇄적으로 다 메모리 상에 올려줘야 합니다.
- 내가 의도한 테스트 대로 수행하려면 복잡해지거나 까다로워지는 경향이 있습니다.
- 결국 Unit Test도 아니게 됩니다.
- 내가 원하는 부분만 테스트 하는 것이 아닌, 의존성 있는 모든 부분도 연쇄적으로 다 테스트 해야하기 때문
이를 해결하기 위해 Mock 이라는 개념이 존재합니다.
Mock은 다른 포스팅에서 추후 다룰 예정입니다.
Reference
https://velog.io/@chaerim1001/Java-JUnit5-기초-정리
'Unit Test' 카테고리의 다른 글
[Unit Test] MockK (2) - mockkObject, mockkStatic, mockkConstructor (0) | 2024.09.30 |
---|---|
[Unit Test] MockK (1) - Mock, Spy (0) | 2024.08.11 |
[Unit Test] JUnit5 (2) - Parameterized Test (0) | 2024.07.08 |