Skip to content

2023.9.3 일일 학습 기록 - JUnit5 기본 사용방법 #14

Description

@ChanJun-Park

JUnit Basics

JUnit 는 테스트 프레임워크이다. 테스트 프레임 워크는 테스트 작성에 필요한 api 들을 제공하고, 테스트를 실행하며, 성공한 테스트와 실패한 테스트들의 리포트를 제공해주는 역할을 한다.

안드로이드 스튜디오에서는 테스트 프레임워크로 JUnit 을 자동으로 세팅한다.

JUnit5

안드로이드 스튜디오에서 자동으로 설정하는 테스트 프레임워크는 JUnit4 이다. 현재는 JUnit5 가 나와서 더 많은 테스트 기능을 제공하고 있고, 유닛테스트를 작성할때 이러한 기능을 사용하는 것이 도움이 되기 때문에 JUnit5 를 사용하는 것이 좋다.

JUnit5 를 사용하기 위해서는 다음과 같은 의존성 추가와 그레이들 플러그인을 추가할 필요가 있다.

plugins {
	id 'de.mannodermaus.android-junit5' version "1.9.3.0"
}
dependencies {
	testImplementation "org.junit.jupiter:junit-jupiter-api:5.9.3"
	testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.9.3"
	testImplementation "org.junit.jupiter:junit-jupiter-params:5.9.3"
}

AssertK

assertion 을 좀 더 가독성 있게 작성할 수 있도록 도와주는 라이브러리이다. assertion 이란 테스트를 하고자 하는 기능이 정확한 동작을 수행했는지를 검증하는 api 이다.

다음 코틀린 전용 assertion 라이브러리를 사용하면 도움이 된다.

testImplementation "com.willowtreeapps.assertk:assertk:0.26.1"
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class ExampleUnitTest {

    @BeforeEach // 각각의 테스트 메소드가 실행되기 전에 수행됨, 주로 새로운 인스턴스나 상태를 생성하기 위해 사용
    fun setUp() {

    }

    @AfterEach  // 각각의 테스트 메소드가 종료된 후 실행됨. 자원 정리같은 작업에 사용됨.
    fun tearDown() {

    }

    @Test
    fun addition_isCorrect() {
//        assertEquals(4, 2 + 2)
        assertThat(2 + 2).isEqualTo(4) // assertK 를 통해 가독성 향상
    }
}

Repeated tests

JUnit5 에서는 @RepeatedTest 어노테이션을 통해서 테스트를 여러번 수행할 수 있다. 동시성 프로그래밍을 하다보면 스레드 실행 순서에 따라서 레이스 컨디션 등의 문제가 간헐적으로 발생해 기능이 동작하거나 동작하지 않는 현상이 있을 수 있다. 이때 @RepeatedTest 를 통해서 여러번 기능을 수행해봄으로써 동시성 문제가 개선되었는지를 확인할 수 있다.

@RepeatedTest(value = 100)
fun `Add Product with negative quantity, throw Exception`() {
	// Given
	val product = Product(
		id = 0,
		name = "Ice cream",
		price = 5.0
	)
	assertFailure {
		shoppingCart.addProduct(product, -4)
	}
}

Parameterized tests

JUnit5 에서는 다양한 파라미터들을 이용해서 테스트를 여러번 수행할 수 있게 해준다.

@ParameterizedTest  // 테스트에 파라미터를 선언할 수 있게 해준다.
@ValueSource(       // 테스트에 어떤 파라미터를 넣을것인지 선언
	ints = [1, 2, 3, 4, 5]
)
fun `Add Multiple product, total count is correct`(quantity: Int) {
	// Given
	val product = Product(
		id = 0,
		name = "Ice cream",
		price = 5.0
	)
	shoppingCart.addProduct(product, quantity)

	// When
	val totalCost = shoppingCart.getTotalCost()

	// Then
	assertThat(totalCost).isEqualTo(quantity * 5.0)
}

다음과 같이 예상 결과 값을 함께 파라미터로 전달해줄 수도 있다.

@ParameterizedTest
@CsvSource(
	"0,0.0",
	"1,5.0",
	"2,10.0",
	"5,25.0",
	"6,30.0",
)
fun `Add Multiple product, total count is correct`(
	quantity: Int,
	expected: Double
) {
	// Given
	val product = Product(
		id = 0,
		name = "Ice cream",
		price = 5.0
	)
	shoppingCart.addProduct(product, quantity)

	// When
	val totalCost = shoppingCart.getTotalCost()

	// Then
	assertThat(totalCost).isEqualTo(expected)
}

How to test private functions?

private 접근자를 가지고 있는 메소드는 일반적으로 테스트 코드에서 접근이 불가능하다. @OpenForTesting 이라는 어노테이션을 붙이고 private 접근 제한자를 떼서 사용할 수도 있지만 권장되는 방법이 아니다.

대신 private 접근자를 사용하고 있는 메소드를 테스트 함으로써 간접적으로 private function 을 테스트 할 수 있다.

@Test
fun `Add invalid products, getTotalCost is zero`() {
    // Given
    val product = Product(
        id = 123,
        name = "Ice cream",
        price = 5.0
    )

    // When
    cart.addProduct(product, 5)

    // Then
    val totalCost = cart.getTotalCost()
    assertThat(totalCost).isEqualTo(0.0)
}

HomeWork

JUnit5 에서 List 를 테스트 인자로 전달하는 방법

[JUnit 5: Using Lists as an Argument for Parameterized Tests – Felix Seifert](https://blog.felix-seifert.com/junit-5-parameterized-tests-using-lists-as-argument/#)

JUnit5 에서는 @ParameterizedTest 라는 어노테이션을 이용해서 테스트 함수에 인자를 전달할 수 있다. @ParameterizedTest 를 이용할때는 반드시 테스트 함수에 전달할 인자를 제공해주는 Source 를 선언해야한다.

가장 간단한 형태의 Source 는 @ValueSource 이다.

@ParameterizedTest  // 테스트에 파라미터를 선언할 수 있게 해준다.
@ValueSource(       // 테스트에 어떤 파라미터를 넣을것인지 선언
	ints = [1, 2, 3, 4, 5]
)
fun `Add Multiple product, total count is correct`(quantity: Int) {
	// Given
	val product = Product(
		id = 0,
		name = "Ice cream",
		price = 5.0
	)
	shoppingCart.addProduct(product, quantity)

	// When
	val totalCost = shoppingCart.getTotalCost()

	// Then
	assertThat(totalCost).isEqualTo(quantity * 5.0)
}

결과값도 함께 전달하고 싶다면 @CsvSource 를 이용할 수 있다.

@ParameterizedTest
@CsvSource(
	"0,0.0", // 콤마로 구분된 2가지 인자를 테스트 함수에 전달
	"1,5.0",
	"2,10.0",
	"5,25.0",
	"6,30.0",
)
fun `Add Multiple product, total count is correct`(
	quantity: Int,
	expected: Double
) {
	// Given
	val product = Product(
		id = 0,
		name = "Ice cream",
		price = 5.0
	)
	shoppingCart.addProduct(product, quantity)

	// When
	val totalCost = shoppingCart.getTotalCost()

	// Then
	assertThat(totalCost).isEqualTo(expected)
}

인자를 생성하는 팩토리 메소드를 이용해서 동적으로 인자들을 생성하고 싶다면 @MethodSource 를 이용할 수 있다.

@ParameterizedTest
@MethodSource(
    "createArguments" // 인자를 생성하는 팩토리 메소드 이름을 전달
)
fun `params test`(param1: Int, param2: Int, param3: Int) {
    assertThat(param1 + param2).isEqualTo(param3)
}

private fun createArguments(): Stream<Arguments> { // 여러개의 인자를 생성하는 경우 Stream<Arguments>
    return Stream.of(                              // 한개의 인자만 생성하면 되는 경우 Stream<인자 타입>
        Arguments.of(1, 2, 3),
        Arguments.of(1, 1, 2),
        Arguments.of(1, 5, 6),
    )
}

@ParameterizedTest
@MethodSource(
    "createListArguments" // 인자를 생성하는 팩토리 메소드 이름을 전달
)
fun `list params test`(listParam: List<Int>, param2: Int) {
    assertThat(listParam.sum()).isEqualTo(param2)
}

private fun createListArguments(): Stream<Arguments> {
    return Stream.of(
        Arguments.of(listOf(1, 2, 3), 6),
        Arguments.of(listOf(1, 2, 4), 7),
    )
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions