AVA—深化:JUnit


JUnit



この文章はJUnitを使ってテストを書く基本的な方法についての文章です.このテーマは,企画とサーバ開発を並行した経験から得られた個人的な見解である.「テスト」という言葉は最近人気のある言葉ですが、存在理由があり、必要な知識や役に立つスキルとされています.
まず,サービスの計画やサーバの開発において感じたことは,おおむね次のようなものである.
  • 要求が明確であればあるほど、具体的であればあるほど、機能が具体的であればあるほど、信頼性が高い
  • システムの統合過程は簡単ではなく、意外な誤った戦争である.
  • サービスの機能的要素は開発のすべてではなく,非機能性(性能,信頼性など)も同様に重要である.
    このような過程を経て、どのようにプロジェクトを完成させ、どのように企画者や開発者として安定的かつ効率的にプロジェクトを運営するかについて、「テスト優先主義」という結論が出た.
    ここで最も重要なのは「必ずテストがあり、テストを通過させる」ことではなく、入力された出力を事前に明確かつ具体的に認識することです.このプロセスを通じて、ニーズをより深く分析し、ニーズをより細かく理解することができます.開発者の観点から、この基礎の上で明確で信頼できる機能を開発し、システム統合の中で古いコードに対する信頼に基づいて、効率的で、安定的に統合することができます.
    また,実際には開発時にInputとOutputの予想と実際の値を比較し,要求が明確に実行されたか否かを判断し,JavaはJUnitを代表として用いる.だから私個人はこれが必要性だと思って、学習と編纂の基礎的な例で、みんなが楽な気持ちで読むことができることを望みます.

    JUnit 5


    JUnitにはいくつかのバージョンがあり、現在5つのバージョンが最新で、以下の構造から構成されています.
  • プラットフォーム:JUnitのテストエンジンインタフェースを持ち、テスト発見->実行->レポート
  • Jupiter:API
  • にJUnit 5の機能が含まれている
  • Vintage:API
  • 以前のJUnitバージョン
    この記事は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 BransのJUnit 5 Basicsビデオを見に行きます