-
JUnitWeb-Spring 2023. 12. 12. 02:26
JUnit 5 의 간단한 개념
아래의 링크를 참고 하였다.
JUnit 5
The JUnit team uses GitHub for version control, project management, and CI.
junit.org
JUnit 5는 JVM을 테스팅 하기위한 개발자에게 제공하는 테스팅 프레임워크 이다.
JUnit의이전 버전들과는 달리 Junit5는 세 가지 다른 특징으로 구성되어있다.
JUnit 5 = JUnit Platform + JUnit jupiter + JUnit Vintage
JUnit Platform
JUnit Platform (이하 플렛폼) 은 JVM에 대한 테스팅 프레임워크를 실행하기 위한 기반을 제공한다.
또한 플렛폼 위에서 동작하는 테스팅 프레임워크를 개발하기 위한 TestEngine API가 정의되어있다.
JUnit Jupiter
JUnit Jupiter(이하 주피터) 는 JUnit5에서 테스트를 작성 하고 확장하기위한 프로그래밍 모델 과 확장 모델의 조합이다.
프로그래밍 모델은 테스트 정의 및 해당 테스트코드들의 실행순서 등을 정의할 때 사용되는 어노테이션과
Assertions, Assumtions등을 말하는 것
Assertions 와 Assumtions는 아래쪽에서 JUnit 사용 예시에서 자세하게 알아보자.
JUnit Vintage
Junit Vintage(이하 빈티지) 는 플렛폼에서 Junit3나 Junit4 기반으로 된 테스트들이 작동하도록 TestEngine을 제공한다.
대충 저런 구성요소로 이루어져있다 정도만 알아보면 될 것 같다. 이제 테스트를 작성 해 보자.
테스트 작성해보기
JUnit5 테스트 준비
JUnit5 테스트에 앞서, 본 포스팅은 아래의 스펙으로 진행한다.
IDE : IntellJ
Java : 17
Build : Maven
우선 intellj 에서 새 프로젝트 만들기에서 Build System 을 Maven으로 선택 후 만든다.
Calculator 클래스를 만들어서 아래의 코드를 작성한다.
public class Calculator { public int add(int a, int b){ return a + b; } public int subtract(int a, int b){ return a - b; } }
그리고 루트 폴더에 우클릭 > new > directory를 누르고 폴더명은 test로 만든다.
작성이 완료된 클래스 Calculator 에 대하여 우클릭 > Go To > Test 를 누른다.
그럼 아래와 같은 창이 나오게 되는데
여기서 Create New Test... 를 누른후 아래와 같이 작성한다.
그러면 JUnit 5가 없어서 에러가 뜰텐데
pom.xml에 디펜던시를 추가해야 한다.
https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api/5.10.1
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>JUnitExample</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.10.1</version> <scope>test</scope> </dependency> </dependencies> </project>
준비는 다되었고
완료된 MyTest의 모습은 다음과 같다.
package org.example; import static org.junit.jupiter.api.Assertions.*; class MyTest { @org.junit.jupiter.api.Test void add() { } @org.junit.jupiter.api.Test void subtract() { } }
현재 어노테이션들이 풀패키지명으로 되어있는 부분을 리펙토링 해 주고,
우리가 테스트 할 대상 클래스는 Calculator 이므로, 해당 클래스를 import 한다.
package org.example; import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; class MyTest { private final Calculator calculator = new Calculator(); @Test void add() { } @Test void subtract() { } }
이제 주요 어노테이션과 함께 여러 테스트들을 해보자
어떤 어노테이션들이 주요 어노테이션인지는 아래를 참고하였다.
https://donghyeon.dev/junit/2021/04/11/JUnit5-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C/
@Test
해당 메소드가 Test 메소드임을 나타내는 어노테이션이다.
간단하게 넘어가자
@DisplayName
테스트 클래스 혹은 테스트 메소드에 대해 어떤 테스트인지 정의하는 어노테이션이다.
사용법은 @DisplayName("설명 할 내용") 으로 사용한다.
우선 그냥 add() 를 실행 해보면 아래와 같다.
아래와 같이 @DisplayName 어노테이션을 추가하고 결과를 보면 이름이 붙게된다.
@Test @DisplayName("add method test") void add() { }
추가적으로
JUnit 에는 @DisplayNameGeneration을 통해
현재 메소드의 이름과 클래스 명을 조합하여 다양한 @DisplayName을 만들 수 있다.
@DisplayNameGeneration
사용법은 @DisplayNameGeneration(DisplayNameGenerator. ~~.class) 로서
테스트 메소드들이 있는 클래스 위에 작성하면 된다.
DisplayNameGenerator의 몇몇 클래스를 들고옴으로서 다양한 사용이 가능한데
각각의 클래스는 다음과 같은 의미를 가진다.
Standard : 테스트 메소드들의 이름과 괄호를 포함
Simple : 테스트 메소드들의 이름만 포함
ReplaceUnderscores : 언더바가 있는곳을 제거하고 공백으로 교체 하여 사용
IndicativeSentences : 클래스명 + 테스트 메소드이름 + 괄호 까지
@AfterEach & @BeforeEach
~~Each가 붙었기에, 뜻 그대로 각각의 테스트 메소드의 실행 전(before) 혹은 후(after) 마다 실행된다
@AfterEach
각각의 @Test 어노테이션이 붙은 메소드들이 실행 된 후 실행된다.
class MyTest { private final Calculator calculator = new Calculator(); @AfterEach void after(){ System.out.println("테스트 실행 후 실행!!!"); } @Test void add() { System.out.println("add 테스트 진행!!!"); } @Test void subtract() { System.out.println("before 테스트 진행!!!"); } }
AfterEach 어노테이션이 붙은 메서드가 테스트 후 실행된 모습 @BeforeEach
각각의 @Test 어노테이션이 붙은 메소드들 전에 실행되어야 하는 메소드에 붙이는 어노테이션이다.
주로 테스트 전 미리 데이터를 만들어 놓을 때 사용한다.
class MyTest { private final Calculator calculator = new Calculator(); private int a; private int b; @BeforeEach void before(){ a = 10; b = 20; } @Test @DisplayName("add method test") void add() { System.out.println("add method a : "+a +" b : "+b); } @Test void subtract() { System.out.println("subtract a : "+a +" b : "+b); } }
위의 테스트 클래스를 대상으로 실행하게 되면 아래와 같은 결과가 나온다.
여기서 보면 테스트 작성순서는 add() -> subtract() 순으로 작성했는데
subtract()가 먼저 호출되는것을 볼 수 있다.
JUnit 은 test의 실행순서를 따로 잡아주지 않는다면, 테스트의 순서를 보장하지 않는다.
이러한 테스트 실행순서를 고정시켜주는 어노테이션이 있다.
@TestMethodOrder(~~.class) & @Order(순번)
JUnit은 일반적으로 순서를 정해주지 않는다면, 작성 한 순서대로 작동을 보장하지 않는다.
따라서 이 테스트 클래스의 메소드들이 어떤 순서가 있다라는 것을 정의하는 @TestMethodOrder 어노테이션과
메소드별로 순번을 매기는 @Order 어노테이션을 함께 사용하여
테스트의 순서를 강제 할 수 있다.
순서에 대한 정의는 MethodOrderer에 있다.
일반적으로 OrderAnnotation을 들고오면 @Order에 순번에 따라 테스트를 실행한다.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) class MyTest { private final Calculator calculator = new Calculator(); @Test @DisplayName("add method test") void add() { } @Test void subtract() { } @Test void divide(){ } }
하지만 위의 상황에서는
해당 테스트 클래스 내 메소드가 @Order 어노테이션의 순번에 따라 작동한다고 정의만 내렸을 뿐
실제로 @Order 어노테이션이 메소드에 붙지 않았으므로 여전히 순서를 보장할 수 없다.
작성된 순서와 실행순서가 다른 모습 따라서 아래와 같이 @Order 어노테이션을 함께 사용한다면
개발자가 매긴 순번에 따라 테스트 메소드의 실행순서가 강제화 된다.
@Test @Order(1) void add() { } @Test @Order(2) void subtract() { } @Test @Order(3) void divide(){ }
순서가 드디어 잡혔다! @AfterAll & @BeforeAll
@AfterAll
모든 테스트 메소드가 실행된 후 실행된다.
~Each와는 달리, 각각의 메소드가 실행 된 후 실행되는것이 아니다.
@BeforeAll
모든 테스트 메소드가 실행되기 전 실행된다.
@AfterAll 을 테스트해보려 하니 아래와 같은 에러가 뜬다.
org.junit.platform.commons.JUnitException: @AfterAll method 'void org.example.MyTest.afterAll()' must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).
대충 번역해보면 AfterAll 어노테이션이 달린 메소드는 static이어야 하거나
적어도 해당 테스트 클래스에 @TestInstance(Lifecycle.PER_CLASS) 어노테이션이 달려야 한다고 되어있다.
왜 이런 문제가 발생하는지에 대해선, 테스트 라이프 사이클에대해 간단하게 알아봐야 한다.
테스트 라이프 사이클
JUnit에서는 테스트 메서드를 독립적으로 실행시키기 위해
각각의 테스트 메서드를 실행하기 전 테스트 클래스의 새로운 인스턴스를 만들게 된다.
이것이 디폴트 동작이며
@TestInstance 어노테이션을 통해 인스턴스의 생성 주기를 제어할 수 있다.
위의 디폴트 동작을 어노테이션으로 나타내면 @TestInstance(LifeCycle.PER_METHOD)가 된다.
만약 같은 인스턴스 내에서 모든 테스트 메소들을 실행하고 싶다면
@TestInstance(LifeCycle.PER_CLASS)를 테스트 클래스에 달아주면 된다.
이렇게 하면 각각의 테스트 메서드들이 테스트 클래스 내 인스턴스 변수를 공유하게 되므로
몇몇 테스트 메서드들이 서로다른 인스턴스 값을 사용해야 한다면
@AfterEach나 @BeforeEach를 통해 인스턴스 변수의 값을 조정해야 한다.
다시 본론으로 넘어가서 저 익셉션이 요구하는 사항은
현재 테스트 인스턴스 생성주기를 클래스마다로 변경하던지 static으로 만들던지 하라 라는것이 된다.
@TestInstance(TestInstance.Lifecycle.PER_CLASS) class MyTest { private final Calculator calculator = new Calculator(); @AfterEach void after(){ System.out.println("테스트 실행 후 실행!!!"); } @AfterAll void afterAll(){ System.out.println("모든 테스트 진행 후 실행!!!"); } @BeforeAll void beforeAll(){ System.out.println("모든 테스트 진행 전 실행!!!"); } @Test void add() { System.out.println("add 테스트 진행!!!"); } @Test void subtract() { System.out.println("before 테스트 진행!!!"); } }
@BeforeAll 과 @AfterAll 이 잘 실행되고 있다. @Disabled
테스트 메소드 혹은 테스트 클래스에 대하여 해당 테스트를 실행하지 않게 된다.
class MyTest { private final Calculator calculator = new Calculator(); @Test void add() { System.out.println("add 테스트 진행!!!"); } @Test @Disabled void subtract() { System.out.println("before 테스트 진행!!!"); } }
이것들 외에도 @Nested, @ParameterizedTest 등이 있으나 나중에 알아보도록 한다.
이제 Assertions과 Assumtions에 대해 알아보자
Assertions
Assertion : 그대로 번역하면 주장이고
테스트가 내가 리턴하기를 예상하는 값을 제대로 리턴하는지 확인하는 메소드들이 있는 클래스이다.
JUnit4가 가지고 있는 assertion 메소드와 Java 8의 람다식이 적용된 일부 assertion 메소드들을 가지고 있다.
이러한 assertion 메소드들은 모두 static이다.
아래의 assert 메소드들의 활용을 보자.
class MyTest { private Calculator calculator; private int a; private int b; @BeforeEach public void before(){ calculator = new Calculator(); a = 20; b = 10; } @Test @Order(1) public void standardAssertions(){ assertEquals(30, calculator.add(a, b)); // assertEquals(5, calculator.subtract(20, 10), "실패 했을 경우"); // 실패 메세지는 Optional로서 있어도 되고 없어도 되지만 // 마지막 파라미터로 넣어줘야 한다. // 실패했을 경우 보고 할 메시지는 람다식으로도 가능하다. assertTrue(a == b, ()->"실패했을 경우 메시지를 이렇게 람다식으로 적어도 된다."); // assertTrue는 true일 경우 성공이며 // 두 번째 인자로는 실패했을 경우 보고 할 메시지가 된다. assertEquals(10, calculator.subtract(a,b)); // assertTrue에서 실패하기 때문에 위의 assert는 실행되지 못한다. } @Test @Order(2) public void groupedAssertions(){ // 해당하는 assert 메소드들의 실패에 대한 보고가 모두 함께 표현된다. // 따라서 위의 상황처럼 assert메소드 중 실패가 있더라도 모두 실행된다. assertAll("group1", ()-> assertEquals(100, calculator.subtract(a, b)), ()-> assertEquals(15, calculator.subtract(a, b)) ); } @Test @Order(3) public void exceptionAssertions(){ // 첫 번째 인자에 어떤 예상되는 예외인지 // 두 번째 인자에 Excutable로서 람다식을 통해 메소드 실행 // assertThrows() 는 제네릭 상한선이 Throwable이므로 Exception으로 받을 수 있다. Exception exception = assertThrows(NullPointerException.class, ()->calculator.divide(1, 0)); } @Test @Order(4) public void timeoutExceeded(){ // 첫번째 인자로 제한시간, 두번째 인자로 람다식을 통해 실행할 메소드를 실행하는데 // 이 때 assertTimeout() 메소드의 타입이 두번째 인자 람다식의 타입으로 결정된다. // 아래의 예시는 Thread.sleep(100)을 이용하여 // 제한시간 10ms보다 람다식의 실행시간이 오래걸리게 되고, 결국 fail하게 만들기 때문에 // 마지막 인자의 String 값이 fail과 함께 보고된다. assertTimeout(Duration.ofMillis(10),()->{ Thread.sleep(100); },"실패 메시지"); } }
assert~()에서 마지막 파라미터로 실패했을 경우의 메시지 문자열 혹은 람다식이 오게된다.
람다식의 경우 해당 람다식이 리턴한 문자열이 실패 메시지로 전달된다.
위를 실행하면 아래와 같게 된다.
두번째로 assertAll 이 있는데
이는 두번째 인자로 람다식으로 정의된 assert~ 메소드들을 실행한다.
이 때 assert~들을 여러개 실행하는것과 assertAll 로 묶어서 실행하는 것의 차이로는
assert~ 메소드들은 기본적으로 실행 도 중 assert~ 하나가 fail나면 그 아래의 assert 메소드들은 실행하지 못한다.
하지만 assertAll 로 묶어서 정의 한다면
기본적으로 모두 실행 한 후 실패한 경우에 대해서 각각을 보고하게 된다.
따라서 groupedAssertions() 테스트 메소드를 실행하게 된다면 아래와 같아진다.
exceptionAssertions 는 exception 발생을 예상하는 assert 메소드로서
assertThrows() 를 사용하고
사용법으로는 assertThrows(Exception.class, ()-> 실행할 메서드) 가 된다.
assertionThrows의 타입이 < ? extend Throwable> 이므로 Exception 타입으로 받아준다.
그렇게 받은 Exception 객체의 상태를 assertEquals() 메소드로 비교하여 exception이 의도한 대로 발생하였는지 검사한다.
exceptionAssertions 메서드를 실행하기 앞서 Calculator 클래스에 divide 메서드를 추가하자.
public int divide(int a, int b){ return a/b; }
그리고 exceptionAssertions를 실행한다면 아래와 같아진다.
assertTimeout() 의 경우 assertThrows() 처럼 리턴하는 값이 존재하며
assertTimeout() 의 타입은 두번째 인자로 들어오는 람다식의 타입과 동일하게 된다.
두번째 인자의 메서드 실행이 설정 해 둔 한계시간을 넘어간다면 fail 이 된다.
timeoutExceeded() 테스트 메소드를 실행하면 아래와 같은 fail이 발생한다.
Assumptions
Assumtions : 말뜻 그대로 가정하는 것이다.
Assumtions는 주어진 테스트 메소드를 실행하는것이 타당하지 않을때 사용된다.
실패한 assumption은 "테스트 실패"라는 결과가 아닌
오히려 테스트가 중단되는 결과를 도출한다.
테스트가 현재 런타임 환경에서 존재하지 않는 변수 등에 의존하고 있을 때
Assumtion을 사용하여 상황에 따른 테스트를 진행여부를 결정지을 수 있다
JUnit 주피터는 JUnit4에서 제공하는 assumptions 에 Java 8의 람다 표현식과 메소드 참조가 더해진 assumption을 사용한다.
JUnit 주피터의 assumptions는 static 메소드 이다.
아래의 예시코드를 작성 하고 실행 해 보자
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.Assumptions.assumingThat; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class MyTest { private int a; private int b; private String ENV; @BeforeEach public void before(){ a = 20; b = 10; ENV = "KHC"; } @Test @Order(1) void testOnlyOnKHC(){ //assumeTrue의 경우 // 첫번째 인자로 boolean 타입의 표현식 // 두번째 인자로 실패했을 경우 메시지를 람다식 혹은 String형태로 담을 수 있다. // assumeTrue가 실패했을 경우 해당 테스트 메소드의 진행을 중단하게 된다. // assertTrue(a==b, "a != b"); << 이 주석을 해제하게 된다면 assumeTrue까지 닿지 않게 된다. System.out.println("bla bla"); // 여기까지는 assumeTrue의 영향 밖이므로 실행되게 된다. assumeTrue("KHC".equals(ENV), ()-> "now env is not KHC"); assertTrue(a != 20, ()->"a not 20"); System.out.println("저는 작동해요!"); } @Test @Order(2) void testOnlyOnKOI(){ //assumingThat의 경우 assumeTrue와는 달리 테스트메소드의 실행을 중단시키진 않는다. // 첫번째 인자로 boolean 타입의 표현식 // 두번재 인자로 assumption에 성공했을 때 실행할 assert를 정의 assumingThat("KOI".equals(ENV), ()->{ assertAll( ()->{ assertTrue(a != 20, ()->"a is "+a); }, ()->{ assertTrue(b != 10, ()->"b is "+b); } ); // assertAll 내부 람다식 assert가 하나라도 실패했기 때문에 // 아래의 asertTrue는 실행되지 못한다. assertTrue(a == b, ()->"a is not equal b"); }); // assumingThat의 경우 assumingThat으로 둘러싸인 부분만 영향받기 때문에 전체 메소드 실행을 제어하지 않는다. assertTrue(a == b, ()->"b is not equal a"); } }
기본적으로 assumeTrue() 와 assumingThat() 이 있다.
이 둘의 차이점은
assumeTrue()는 메소드가 진행하다가 assumeTrue()를 만났는데 실패한다면
더 이상 해당 테스트 메소드의 진행을 멈추게 된다.
아래는 진행이 중단된 모습이다.
하지만 assumingThat의 경우 단순히 assumeption이 true/false 여부에 따라
내부적인 람다식으로 실행되는 assertion 메소드를 실행 여부가 결정되는 것으로
해당 테스트메소드 전체의 실행을 중단시키지 않는다.
따라서 아래의 실행 결과사진을 보면
assumingThat의 두번째 인자는 실행되지 못했지만
그 아랫줄에 assertion 메소드는 실행되는 모습을 볼 수 있다.
'Web-Spring' 카테고리의 다른 글
@JsonTypeInfo 를 통해 유연하게 Json과 Java객체 매핑하기 (0) 2025.04.14 mybatis & hikari 설정 (9) 2023.12.05