JAVA - 심화 : JUnit

Seok-Hyun Lee·2021년 8월 20일
0

JAVA

목록 보기
21/21

JUnit


이번 포스트는 JUnit 을 사용해 테스트를 작성하는 기초적인 방법에 대한 글이다. 이번 주제는 사실 기획과 서버 개발을 병행했던 경험에서부터 우러나온 개인적인 견해로부터 나왔다. "테스트"란 단어가 요즘 핫한 용어이긴 한데 그럴만한 이유가 있고, 꼭 필요한 지식이자 유용한 기능이라고 생각된다.

우선, 서비스를 기획하고 서버 개발을 하면서 느꼈던 점은 대충 아래와 같다.

  • 요구사항이 명확하고 구체적일수록 기능이 구체적이며 신뢰성이 증가한다
  • 시스템 통합 과정은 단순하지 않으며 예상치 못한 에러와의 전쟁이다.
  • 서비스의 기능적 요소만이 개발의 전부가 아니다, 비기능적(성능, 신뢰성 등)도 동등히 중요하다

이런 과정을 거치면서 프로젝트를 마무리하고 어떻게 하면 기획자로서 또는 개발자로서 프로젝트를 안정적이고 효율적으로 운영할 수 있을까를 고민하고 내린 결론은 "테스트 우선주의"였다.

여기서 중요한 것은 "테스트가 무조건 있어야하고, 테스트를 통과하도록 만들자"가 아니라 인풋에 대한 아웃풋을 미리 명확하고 구체적으로 인지를 해야한다는 것이 가장 중요하다. 이 과정을 통해 요구사항을 좀더 면밀히 분석하고 꼼꼼하고 구체적으로 파악할 수 있고, 개발하는 사람 입장에서는 이를 토대로 명확하고 안정적인 기능을 개발하고 시스템 통합에서도 이전 코드에 대한 신뢰를 기반으로 효율적이고 안정적이게 통합할 수 있다.

그리고 실질적으로는 개발시 인풋과 아웃풋의 예상과 실제값을 비교하여 요구사항을 명확히 수행하였는지 확인할 수 있는 테스트를 진행하고 Java 에서는 JUnit을 대표적으로 사용한다. 그래서 개인적으로 이에 대한 필요성을 느끼고 공부하며 작성한 기초적인 예제이니 가벼운 마음으로 읽으면 좋겠다.

JUnit 5

JUnit 에는 여러가지 버전이 있고 현재 5 버전이 최신이고 아래와 같은 구조로 이루어져 있다.

  • Platform : JUnit의 TestEngine 인터페이스 보유, 테스트 발견 -> 실행 -> 보고
  • Jupiter : JUnit 5 에 해당하는 기능들의 API
  • Vintage : 이전 JUnit 버전들의 API

이번 글은 JUnit을 활용해 테스트를 활용하는 방법에 대한 글이기 때문에 JUnit에 대한 자세한 내용은 공식 문서에서 확인하도록 하자.

Code to Test

우선, 테스트를 진행하려면 그 대상이 될 소스 코드, 즉 기능이 있어야 하기 때문에 아래처럼 간단한 연산을 하는 메소드를 정의했다.

public class MathUtils {
    public int add(int a, int b) {
        return a + b;
    }

    public double computeCircleArea(double radius) {
        return Math.PI * radius * radius;
    }

    public int subtract(int a, int b){
        return a -b;
    }

    public int multiply(int a, int b){
        return a * b;
    }

    public int divide(int a, int b){
        return a / b;
    }
}

Code for Test

테스트를 진행하는 코드는 전체 포스트의 가독성을 높이기 위해 따로 글을 작성하지 않고, 소스 코드를 읽기만 해도 이해가 가도록 주석처리 하였다.

package io.shlee7131;

import jdk.jfr.Enabled;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;

import static org.junit.jupiter.api.Assertions.*;

// 테스트를 돌리기 위해 MathUtilsTest 객체는 어떻게 생성되고 Run을 하는가?
// JUnit 에서는, 테스트 클래스의 테스트 메소드 당 1개씩의 테스트 객체를 만들어 실행
// 이로 인해, 각 테스트는 개별적으로 진행가능, 즉 테스트 메소드 당 독립적인 테스트 객체가 생성 및 실행
// 여기서 주의할 점은, 테스트 클래스의 멤버 변수가 여러 테스트 메소드에서 사용되며 값이 변경될 수 있는 지의 여부 판단
// 모두 독립적인 객체이기 때문에 테스트 진행 중 로직 상의 에러 발생 가능

// JUnit Life Cycle ( default : 메소드 단위 생명주기 ) + 관련 어노테이션
// 1. 테스트 클래스에서 객체 초기화 => @BeforeAll(객체 생성 전 최초 실행)
// 2. 테스트 메소드마다 시작되면 새로운 테스트 객체 생성, 메소드 실행 => @BeforeEach(각 메소드를 실행하기 전에 실행)
// 3. 테스트 메소드마다 종료되면 테스트 완료된 객체 삭제 => @AfterEach
// 4. 모든 메소드 종료 시 테스트 객체 삭제 => @AfterAll

// 테스트 객체 생성 옵션 조정 => @TestInstance(TestInstance.Lifecycle.PER_CLASS)
// Default 는 TestInstance.Lifecycle.PER_METHOD => 각 메소드마다 객체 생성
// TestInstance.Lifecycle.PER_CLASS => 단 하나의 테스트 객체에서 모든 테스트 메소드 실행

// Assert Message 최적화를 시키는 방법 => Supplier 사용 ( 람다 )
// ex) assertEquals(expected, actual, () -> "실패시에만 String 생성 및 출력");

@DisplayName("When running MathUtils")
class MathUtilsTest {
    MathUtils mathUtils;

    // 클래스 테스트 객체가 초기화되기 전에 최초 1회 실행
    // 주의할 점은, 클래스의 객체가 없이도 실행되어야 한다 => 즉, static 이어야 한다.
    // 참고로 각 메소드별 테스트에서는 실행되지 않는다.
    @BeforeAll
    static void beforeAllInit(){
        System.out.println("This needs to run before all");
    }

    // 각 테스트 메소드가 실행하기 전마다 시작
    // TestInfo 와 TestReporter 에 대한 의존성 주입 => 메타 데이터 활용
    @BeforeEach
    void init(TestInfo testInfo, TestReporter testReporter){
        // Map 자료구조로써 테스트 시간과 testInfo 를 활용한 메타 데이터 정보 콘솔 출력 => 로그로 활용 가능
        testReporter.publishEntry("This test is : " + testInfo.getDisplayName() + " with a Tag name " + testInfo.getTags());
        mathUtils = new MathUtils();
    }

    @AfterEach
    void cleanup() {
        System.out.println("Cleaning up...");
    }

	// @Test 로 테스트 메소드임을 명시 => 이 어노테이션이 있는 메소드만 테스트 진행
    // 단위 테스트 내부의 객체는 테스트 시작 시 생성, 종료 시 삭제된다.
    // Tag 를 활용해 Test Configuration 에서 Tag 별로 테스트 템플릿 작성 => 별도 테스트 가능
    // Tag 이름에 공백 없어야 함
    @Test
    @Tag("Math")
    @DisplayName("Testing Add")
    void testAdd() {
        int expected = 2;
        int actual = mathUtils.add(1,1);

        // expected 와 actual 이 같은지 확인 => 다르면 예외 발생, 예외 메세지 커스텀 가능
        // assertArrayEquals(expectedArray, actualArray) , assertIterableEquals(expectedArray, actualArray)
        assertEquals(expected,actual, "Return value is not a expected one");
    }

    @Test
    @Tag("Complicated")
    @DisplayName("Testing Compute Circle Radius")
    void testComputeCircleRadius(){
        assertEquals(314.1592653589793, mathUtils.computeCircleArea(10),"Should return right circle area");
    }

    // 테스트 반복 테스트  => 반복 중 하나라도 실패하면 예외 발생
    // @Test 대신에 @RepeatedTest 사용
    // 메소드 매개로 반복정보 사용가능
    @RepeatedTest(3)
    @Tag("Math")
    @DisplayName("Testing Divide")
    void testDivide(RepetitionInfo repetitionInfo){
        System.out.println("This is " + repetitionInfo.getCurrentRepetition() + " trial");
        // 예외 처리 확인을 위한 테스트 => 실행에 대해 해당 예외를 던지는 것이 맞으면 success
        // Fail 에 대한 Message 추가 가능(NullPointerException)
        assertThrows(ArithmeticException.class, () -> {
            mathUtils.divide(1,0);
        }, "Should divide by Zero should throw");
    }

    // 스킵하기
    @Test
    @Tag("Failure_Check")
    @Disabled
    @DisplayName("TDD method. Should not run => Skip this !!")
    void testDisabled(){
        fail("This test should be disabled");
    }


    // OS 옵션 확인
    @Test
    @Tag("Failure_Check")
    @EnabledOnOs(OS.LINUX)
    @DisplayName("Run on Linux")
    void testLinux(){
        fail("This test should be disabled");
    }

    // 다중 테스트, 모두 다 통과하지 못하면 예외 발생
    @Test
    @DisplayName("Mulitply Method")
    void testMultiply(){
        assertAll(
                ()->assertEquals(4,mathUtils.multiply(2,2),"First"),
                ()->assertEquals(0,mathUtils.multiply(2,0),"Second"),
                ()->assertEquals(-2,mathUtils.multiply(2,-1),"Third")
        );
    }

    // Group Multiple Test Methods => 모아서 관리 => 가독성 증가 및 테스트 분석 용이
    @Nested
    @Tag("Math")
    @DisplayName("add method,")
    class addTest{
        @Test
        @DisplayName("when adding two positive numbers")
        void testAddPositive(){
            assertEquals(2,mathUtils.add(1,1),"should return the right sum");
        }

        @Test
        @DisplayName("when adding two negative numbers")
        void testAddNegative() {
            int expected = -2;
            int actual = mathUtils.add(-1, -1);
            // supplier(람다)를 사용해서 message 생성 조건을 실패 시로 한정, Default 는 항상 생성
            assertEquals(expected, actual, () -> "should return the right sum" + expected + " but returned " + actual);
        }
    }
}

참고 자료

위 코드들은 이 분의 영상을 보면서 따로 주석을 넣어 보기 좋게 만든 것이니 원본이 궁금하면 여기를 참고하시길 바랍니다.
=> Java Brains의 JUnit 5 Basics 영상 보러가기

profile
Arch-ITech

0개의 댓글