JUnit5
Java 언어로 작성된 자동화된 단위 테스트 프레임워크
테스트 케이스를 작성하기 쉽게 만들어주어 단위 테스트를 할 수 있도록 도와준다.
다만, 단위 테스트를 위한 프레임워크이기 때문에 주로 클래스, 메소드 등을 테스트할 때 사용한다.
JUnit 은 생각보다 가까이 있었다.
일반적으로 안드로이드 스튜디오 또는 인텔리제이 코틀린 프로젝트를 생성하면 그림 1처럼 main 패키지 말고도
test 패키지가 생성된 것을 볼 수가 있다.
이 것이 IDE 자체에서 JUnit 5으로 테스트를 실행할 수 있는 test 전용 경로로 지정되어있다.
또한 build.gradle에선 이미 JUnit을 Implementation하고 있다.
보다싶이 이미 IDE에서 JUnit을 지원하고 있다.
JUnit 5의 구조
먼저 JUnit 5가 어떻게 이루어져 있는지 알아보자
JUnit 5 는 세 가지 다른 서브 프로젝트(모듈)로 구성되어 있다.
Jupiter, Vintage, JUnit Platform 들이 그 주인공들이다.
1. JUnit Platform
JVM(Java Virtual Machine)에서 테스팅 프레임워크를 실행하는 데 사용되는 기반이다.
후술할 Vintage, Jupiter가 제공하는 TestEngine API를 개발자가 사용할 수 있도록 말 그대로 플랫폼을 수행한다.
많이 알려져 있는 IDE, 예를 들어 인텔리제이, 이클립스, 안드로이드 스튜디오 등과 Gradle, Maven과 같은 빌드 도구들은 이 JUnit Platform을 완벽하게 지원하고 있다.
그렇기 때문에 우리가 평화롭게 잘 사용할 수 있는 것이다.
2. JUnit Jupiter
JUnit 5에서 지원하고자 하는 테스트 + 확장 들을 제공하는 JUnit의 서브프로젝트이다.
JUnit Platform에게 TestEngine API 를 제공한다.
3. JUnit Vintage
JUnit 3 및 JUnit 4 기반 테스트를 실행하기 위한 TestEngine API 제공한다.
쉽게 말해 Jupiter는 최근 기법을 제공하는 API이고 Vintage 는 과거 버전의 호환성을 위한 API라고 생각하면 된다.
JUnit을 알아보았으니 안드로이드에서 어떻게 사용하는 지를 알아보겠다.
안드로이드에선 단위 테스트를 어떻게 할까?
안드로이드엔 두 가지 테스트의 유형이 존재한다.
안드로이드 스튜디오에서 프로젝트를 생성하게 되면 androidTest와 test로 된 폴더 두 종류가 존재한다.
공식 문서에선 androidTest를 계측 테스트(Instrumented tests), test를 로컬 단위 테스트(Local unit test)라고 지칭한다.
1. 로컬 단위 테스트
컴퓨터 로컬 JVM에서 실행되는 테스트이다.
안드로이드 프레임워크에 종속 항목이 없을 경우 사용한다.
쉽게 말해, 안드로이드와 관련된 로직이 아닐 경우 사용하면 실행 시간을 최소화할 수 있다.
2. 계측 테스트(Instrumented tests)
하드웨어 기기나 에뮬레이터에서 실행되는 테스트이다.
Instrumentation 이라는 API를 제공한다.
이를 통해 테스트 중인 앱의 Context와 같은 안드로이드 종속 데이터에 접근할 수 있다.
안드로이드 프레임워크에 종속된다면 사용하면 된다.
이렇게 안드로이드는 크게 두 가지의 테스트가 존재한다.
그런데 문제가 있다.
안드로이드 스튜디오는 기본적으로 JUnit 4를 사용한다.
JUnit 4 말고 5를 써야할 이유가 있을까??
1. JUnit 4는 옛날 버전이다.
당연하게도 JUnit 5는 4의 문제점을 개선하여 나온 버전이다.
그리고 현재도 지속적으로 개발되고 있다.
다만, 완성되지 않았다는 의미이기도 해서 JUnit 4를 아직까지도 많이 선호하는 경향이 존재한다.
(JUnit 5가 아직 안드로이드 스튜디오 IDE를 제대로 지원하지 않는다는 말도 있다.)
JUnit 5는 이를 의식해서인지 JUnit 4의 호환을 위해 Vintage를 만들어서 호환성을 확보했다고 주장한다.
2. 직관적으로 바뀐 Annotation
JUnit 4 | JUnit 5 | 설명 |
@BeforeClass | @BeforeAll | 현재 클래스의 모든 테스트 메서드보다 먼저 실행 |
@AfterClass | @AfterAll | 현재 클래스의 모든 테스트 메서드 실행 후 실행 |
@Before | @BeforeEach | 각 테스트 메서드 전에 실행 |
@After | @AfterEach | 각 테스트 메서드 후에 실행 |
@Ignore | @Disabled | 테스트 메서드/클래스 비활성화 |
@Category | @Tag | 태깅 및 필터링 |
존재 X | @TestFactory | 동적 테스트를 위한 테스트 팩토리 |
존재 X | @Nested | 중첩 테스트 |
존재 X | @ExtendWith | 사용자 정의 확장 |
@RunWith(Parameterized.class) |
@ParameterizedTest | 파라미터화 테스트 |
3. 다양한 기능 존재
조건 기반의 실행을 담당하는 @Enabled와 @Disabled,
테스트의 인스턴스를 관리하는 @TestInstance,
동일한 메서드를 다양한 파라미터로 테스트하는 @ParameterizedTest 등
다양한 기능이 존재한다.
4. 확장 가능성의 존재
내부적으로 플랫폼 위에 돌아가는 방식이다보니
사용자 정의 확장을 작성할 수 있다.
5. 접근 제한자의 제약이 사라짐
기존 JUnit 4에선 public 으로 메소드를 작성하는 것이 관례이자 컨벤션이었다.
하지만 5에 와선 그럴 필요가 없어졌다.
JUnit 5를 안드로이드 스튜디오에서 사용하는 방법
안드로이드 스튜디오는 기본적으로 JUnit 4으로 되어있기 때문에
추가적 설정이 필요하다
mannodermaus/android-junit5: Testing with JUnit 5 for Android. (github.com)
이를 위해 Gradle Plugin 를 사용한다.
app 모듈 build.gradle을 다음과 같이 설정하면 바로 사용할 수 있다.
plugins {
id("de.mannodermaus.android-junit5") version "1.9.3.0"
}
dependencies {
// (Required) 기본 베이스
testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.3")
// (Optional) Parameterized 테스트가 있을 때
testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
// (Optional) JUnit 4 기반 테스트가 있을 떄
testImplementation("junit:junit:4.13.2")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.9.3")
// (Optional) Instrumentation 테스트용
androidTestImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
}
상황에 맞춰 적용하면 된다.
로컬 단위 유닛 테스트 코드 작성 예시 1) 파라미터 테스트
import org.junit.Assert.*
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.CsvSource
class ExampleUnitTest {
@ParameterizedTest
@DisplayName("테스트")
@CsvSource("1, 2", "2, 4", "3, 6", "4, 8")
fun testMultiply(input: Int, expected: Int) {
assertEquals(expected, input * 2)
}
}
CsvSource를 사용하여 파라미터를 테스트하는 간단한 코드이다.
"1, 2" 에서
1이 input이 되고 expected가 2가 되는 형식이다.
로컬 단위 유닛 테스트 코드 작성 예시 2) @DisplayName 사용하여 알아보기 쉽게 하기
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
@DisplayName("리스트 덧셈 테스트")
class UnitTest {
private var numbers: MutableList<Int> = mutableListOf()
@BeforeEach
fun `리스트 초기화`() {
numbers = mutableListOf(1, 2, 3, 4, 5)
}
@Test
@DisplayName("리스트에 숫자 추가하기 테스트")
fun `리스트에 6 추가하기`() {
numbers.add(6)
assertEquals(6, numbers.last())
}
@Test
@DisplayName("리스트에서 3 찾아 삭제")
fun `리스트에서 3 찾아 삭제`() {
numbers.remove(3)
assertEquals(false, numbers.contains(3))
}
@Test
@DisplayName("리스트 내 모든 데이터 덧셈")
fun `리스트 내 모든 데이터 덧셈`() {
val sum = numbers.sum()
assertEquals(15, sum)
}
}
단, 위와 같이 메소드명을 한글로 쓰면 DisplayName을 안써도 알아 볼 수 있다.
로컬 단위 유닛 테스트 코드 작성 예시 3) @Timeout 으로 시간 제한 걸기
class TimeOutTest {
@Test
@Timeout(5, unit = TimeUnit.SECONDS)
@DisplayName("5초 안에 작동해야하는 함수!")
fun testTimeout() {
// 이 테스트는 최대 5초 동안 실행되어야 함
}
}
로컬 단위 유닛 테스트 코드 작성 예시 4) @Nested 으로 테스트 그룹화하기
package com.github.myapplication
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
class Calculator {
fun add(a: Int, b: Int): Int {
return a + b
}
fun subtract(a : Int, b : Int) : Int {
return a - b
}
}
@DisplayName("계산기 클래스 테스트")
class CalculatorTest {
private val calculator = Calculator()
@Nested
@DisplayName("덧셈 테스트")
inner class AdditionTests {
@Test
@DisplayName("양수 덧셈 테스트")
fun testAddPositiveNumbers() {
val result = calculator.add(2, 3)
assertEquals(5, result)
}
@Test
@DisplayName("음수 덧셈 테스트")
fun testAddNegativeNumbers() {
val result = calculator.add(-2, -3)
assertEquals(-5, result)
}
}
@Nested
@DisplayName("뺄셈 테스트")
inner class SubtractionTests {
@Test
@DisplayName("양수 뺄셈 테스트")
fun testSubtractPositiveNumbers() {
val result = calculator.subtract(5, 3)
assertEquals(2, result)
}
@Test
@DisplayName("음수 뺄셈 테스트")
fun testSubtractNegativeNumbers() {
val result = calculator.subtract(3, 5)
assertEquals(-2, result)
}
}
}
@Nested 어노테이션으로 테스트들을 그룹화 시킨 것이다.
inner class에만 @Nested 어노테이션을 사용할 수 있다.
이 때, inner class의 라이프 사이클은 @BeforeAll, @AfterAll 제외된다.
테스트를 계층적으로 만들 수 있다.
이미지 및 내용 출처
JUnit 5 + Kotlin 테스트 클래스에서 생성자 주입 이슈 · 도전하는 개발자 (minkukjo.github.io)
Android 스튜디오에서 테스트하기 | Android Developers
[JUnit] JUnit 5 vs JUnit 4, JUnit Version 4, 5 차이점 (tistory.com)
JUnit 5 (JUnit 4와 비교) — 뀨도리의 디지털세상 (tistory.com)
Junit5의 @Nested를 이용해 테스트 작성해보자 (tistory.com)
'Android' 카테고리의 다른 글
[안드로이드] 어노테이션(Annotation) 개념과 예시 (2) | 2023.11.19 |
---|---|
[안드로이드]RoomDB와 SQLite 비교 (0) | 2023.11.12 |
[안드로이드]Glide에 대하여 (0) | 2023.10.08 |
[안드로이드]Retrofit2 에 관하여 (0) | 2023.09.17 |
[안드로이드]페이징(Paging) - 1 (개념 위주) (0) | 2023.09.10 |