본문 바로가기

Unit Test

[Unit Test] JUnit5 (1) - Use in Android Studio

 

안녕하세요.

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-기초-정리

 

[Java] JUnit5 기초 정리

Junit은 자바에서 많이 사용되는 유닛 테스트 프레임워크이다.

velog.io