Android

[안드로이드] 단위 테스트를 도와주는 JUnit 5

나맘임 2023. 10. 29. 19:33

JUnit5

Java 언어로 작성된 자동화된 단위 테스트 프레임워크

테스트 케이스를 작성하기 쉽게 만들어주어 단위 테스트를 할 수 있도록 도와준다.

다만, 단위 테스트를 위한 프레임워크이기 때문에 주로 클래스, 메소드 등을 테스트할 때 사용한다.

 

JUnit 은 생각보다 가까이 있었다.

그림 1. Test 폴더

일반적으로 안드로이드 스튜디오 또는 인텔리제이 코틀린 프로젝트를 생성하면 그림 1처럼 main 패키지 말고도

 

test 패키지가 생성된 것을 볼 수가 있다.

그림 2. 인텔리제이 내에서 Tests 폴더로 지정되어 있는 모습

 

이 것이 IDE 자체에서 JUnit 5으로 테스트를 실행할 수 있는 test 전용 경로로 지정되어있다.

 

그림 3. 인텔리제이와 안드로이드 스튜디오에 이미 설정되어 있는 JUnit

또한 build.gradle에선 이미 JUnit을 Implementation하고 있다.

 

보다싶이 이미 IDE에서 JUnit을 지원하고 있다.

 

JUnit 5의 구조

그림 4. 전체적인 구조를 나타낸 이미지

먼저 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을 알아보았으니 안드로이드에서 어떻게 사용하는 지를 알아보겠다.

 

안드로이드에선 단위 테스트를 어떻게 할까?

안드로이드엔 두 가지 테스트의 유형이 존재한다.

그림 5. 안드로이드 테스트 폴더

안드로이드 스튜디오에서 프로젝트를 생성하게 되면 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. 확장 가능성의 존재

내부적으로 플랫폼 위에 돌아가는 방식이다보니

 

사용자 정의 확장을 작성할 수 있다.

JUnit 5 User Guide

 

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo

junit.org

 

5. 접근 제한자의 제약이 사라짐

기존 JUnit 4에선 public 으로 메소드를 작성하는 것이 관례이자 컨벤션이었다.

 

하지만 5에 와선 그럴 필요가 없어졌다.

 

 

 

JUnit 5를 안드로이드 스튜디오에서 사용하는 방법

안드로이드 스튜디오는 기본적으로 JUnit 4으로 되어있기 때문에

 

추가적 설정이 필요하다

 

mannodermaus/android-junit5: Testing with JUnit 5 for Android. (github.com)

 

GitHub - mannodermaus/android-junit5: Testing with JUnit 5 for Android.

Testing with JUnit 5 for Android. Contribute to mannodermaus/android-junit5 development by creating an account on GitHub.

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)
    }
}

그림 6. DisplayName으로 알아보기 쉽게 하기

단, 위와 같이 메소드명을 한글로 쓰면 DisplayName을 안써도 알아 볼 수 있다.

그림 7. 첫번째 메소드 @DisplayName 제거

 

로컬 단위 유닛 테스트 코드 작성 예시 3) @Timeout 으로 시간 제한 걸기

class TimeOutTest {
    @Test
    @Timeout(5, unit = TimeUnit.SECONDS)
    @DisplayName("5초 안에 작동해야하는 함수!")
    fun testTimeout() {
        // 이 테스트는 최대 5초 동안 실행되어야 함
    }

}

 

그림 8. TimeOut 어노테이션 테스트

 

로컬 단위 유닛 테스트 코드 작성 예시 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)
        }
    }
}

그림 9. @Nested 테스트

 

@Nested 어노테이션으로 테스트들을 그룹화 시킨 것이다.

 

inner class에만 @Nested 어노테이션을 사용할 수 있다.

 

이 때, inner class의 라이프 사이클은 @BeforeAll, @AfterAll 제외된다.

 

테스트를 계층적으로 만들 수 있다.

 


이미지 및 내용 출처

JUnit 5 User Guide

 

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo

junit.org

JUnit 5 + Kotlin 테스트 클래스에서 생성자 주입 이슈 · 도전하는 개발자 (minkukjo.github.io)

 

JUnit 5 + Kotlin 테스트 클래스에서 생성자 주입 이슈

서론

minkukjo.github.io

Android 스튜디오에서 테스트하기  |  Android Developers

 

Android 스튜디오에서 테스트하기  |  Android Developers

Android 스튜디오를 사용하여 테스트를 생성, 실행, 분석하는 방법입니다.

developer.android.com

[JUnit] JUnit 5 vs JUnit 4, JUnit Version 4, 5 차이점 (tistory.com)

 

[JUnit] JUnit 5 vs JUnit 4, JUnit Version 4, 5 차이점

JUnit을 공부하고 나서 마지막으로 테스트하던 항목 가운데 Test Suites 부분에서 계속 오류가 발생해서 해결 방법을 찾다가 찾게 된 글입니다. JUnit Version 4와 Version 5의 차이점을 항목별로 비교해 놓

dev-handbook.tistory.com

JUnit 5 (JUnit 4와 비교) — 뀨도리의 디지털세상 (tistory.com)

 

JUnit 5 (JUnit 4와 비교)

JUnit5 란? JUnit이란? 더보기 JUnit은 자바 프로그래밍 언어용 유닛 테스트 프레임워크이다. JUnit은 테스트 주도 개발 면에서 중요하며 SUnit과 함께 시작된 XUnit이라는 이름의 유닛 테스트 프레임워크

jade314.tistory.com

Junit5의 @Nested를 이용해 테스트 작성해보자 (tistory.com)