관리 메뉴

공부 기록장 💻

[Java] 테스트 코드란? JUnit 5, AssertJ 를 이용한 테스트 코드 작성 본문

# Language & Tools/Java

[Java] 테스트 코드란? JUnit 5, AssertJ 를 이용한 테스트 코드 작성

dream_for 2022. 11. 8. 15:50

우아한테크코스 프리코스 2주차 미션으로 각 기능의 작동을 테스트하는 코드를 작성하는 것이 프로그래밍 요구사항 중 하나로 추가되었다.
JUnit 5 와 AssertJ를 이용하여 기능 목록이 정상 동작함을 테스트 코드로 확인하여야 한다.
따라서 테스트코드가 무엇인지 알아보고 이를 적용해보자.

우테코 프리코스 2주차 숫자 야구게임에서 제공된 학습용 테스트 코드


stduy 폴더에 간단한 테스트 코드가 추가되어 있는데,
이를 통해 테스트 코드가 어떤 역할을 하며, 어떤 방식으로 작성되어야 하는지 간단히 학습해보고
테스팅에 대한 자세한 내용은 따로 추가적으로 공부해보자.


우선 study 폴더 내 StringTest 클래스 파일을 보면 다음과 같이
org.junit.jupiter.api.Test와 org.assertj.core.api.Assertions의 함수들이 import 되었다.
테스트를 위한 메서드들은 총 5개가 작성되어 있다.

각 메서드의 시작 부분에는 @Test 어노테이션이 추가되어 있다.

package study;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.*;

public class StringTest {

    @Test
    void split_메서드로_주어진_값을_구분() {
      
    }

    @Test
    void split_메서드_사용시_구분자가_포함되지_않은_경우_값을_그대로_반환() {
      
    }

    @Test
    void substring_메서드로_특정_구간_값을_반환() {
     
    }

    @Test
    void charAt_메서드로_특정_위치의_문자_찾기() {
    
    }

    @Test
    void charAt_메서드_사용시_문자열의_길이보다_큰_숫자_위치의_문자를_찾을_때_예외_발생() {
        
    }
}



각 메서드가 어떠한 방식으로 작성되어 있는지 확인해보자.
split_메서드로_주어진_값을_구분() 메서드는 "1,2" 문자열을 ',' 기준으로 각 요소를 구분하여 문자열 배열 result을 만들었다.

첫번째 테스트 코드는 assertThat() 메서드의 인자로는 result 배열을 넣어 주었고, contains("2", "1") 메서드를 통해 배열이 해당 요소들을 포함하고 있는지를 테스트하는 코드이다.
두번째 테스트 코드는 containsExactly() 메서드를 사용했다.

    @Test
    void split_메서드로_주어진_값을_구분() {
        String input = "1,2";
        String[] result = input.split(",");

        assertThat(result).contains("2", "1");
        assertThat(result).containsExactly("1", "2");
    }


해당 메소드를 컴파일 실행을 해보니 다음과 같이 나타났다/
3 actionable tasks: 3 executed' 과 함께 별다른 오류가 발생하지 않음을 확인했다.


이번에는 contans() 함수의 인자에 "3" 을 추가해보았다.
다음과 같이 expected value와 다르다는 빨간 색의 오류가 뜬다.


이번에는 containsExactly() 메서드의 "1" 과 "2"의 순서를 바꿔보았다.
same elements but not in the same order 라는 문구와 함께 에러가 발생했다.
containsExactly()는 result 배열의 정확한 순서까지 테스트하는 메서드임을 확인하게 되었다.



다음은 split_메서드_사용시_구분자가_포함되지_않은_경우_값을_그대로_반환() 메서드를 통한 테스트이다.
다음을 통해 contains() 메서드는 배열의 요소를 포함하는지 여부만 판단한다는 것을 알았다.

    @Test
    void split_메서드_사용시_구분자가_포함되지_않은_경우_값을_그대로_반환() {
        String input = "1";
        String[] result = input.split(",");

        assertThat(result).contains("1");
    }
    
    @Test
    void split_메서드_사용시_구분자가_포함되지_않은_경우_값을_그대로_반환2() {
        String input = "123,45,";
        String[] result = input.split(",");

        assertThat(result).contains("123","45");
    }
    
    @Test
    void split_메서드_사용시_구분자가_포함되지_않은_경우_값을_그대로_반환() {
        String input = "123,45,";
        String[] result = input.split(",");

        assertThat(result).contains("45");
    }


substring_메서드로_특정_구간_값을 반환() 메서드를 통한 테스트를 통해서는
assertThat()의 isEqualTo() 메서드에 대해 학습할 수 있다.
위의 contains() 와 다르게 isEqualTo()는 결과값과 동일한지 테스트를 한다.

    @Test
    void substring_메서드로_특정_구간_값을_반환() {
        String input = "(1,2)";
        String result = input.substring(1, 4);

        assertThat(result).isEqualTo("1,2");
    }
    
    @Test
    void substring_메서드로_특정_구간_값을_반환() {
        String input = "hello my name is";
        String result = input.substring(2, 6);

        assertThat(result).isEqualTo("llo ");
    }


charAt_메서드_사용시_문자열의_길이보다_큰_숫자_위치의_문자를_찾을_때_예외_발생() 메서드를 통해서는 assertThatThrownBy()의 isInstanceOf(), hasMessageContaining() 를 통해 조금 더 확장된 테스트 코드 추가가 가능함을 학습할 수 있다.
"abc"의 chartAt(5) 메서드를 실행하게 되면, StringIndexOutOfBoundsException 에러를 반환하게 되므로,
isInstanceOf()를 통해 이 클래스의 인스턴스가 반환되는지 확인할 수 있으며,
hasMessageContainig()을 통해 해당 에러 메시지 또한 반환되는 지 테스트해볼 수 있다.

    @Test
    void charAt_메서드_사용시_문자열의_길이보다_큰_숫자_위치의_문자를_찾을_때_예외_발생() {
        String input = "abc";

        assertThatThrownBy(() -> input.charAt(5))
                .isInstanceOf(StringIndexOutOfBoundsException.class)
                .hasMessageContaining("String index out of range: 5");
    }

 


JUnit이란?


https://effortguy.tistory.com/113
위 블로그 글을 참고하서 JUnit 이 무엇인지 그 개념을 학습해보자.

JUnit이란 자바 개발자라면 반드시 알고 있어야 하는 테스팅 프레임워크 중 하나라고 한다.
Intellij를 만든 JetBrain 사의 조사 결과에 의하면, 단위 테스트를 하는 개발자는 75% 이고 JUnit을 사용하는 개발자가 그 중 2019년엔 93%, 2020년엔 83%라고 한다.

특히 스프링 부트와 같은 프레임워크애서 웹을 개발하고 있는 경우,
서버를 구동하고 localhost:8000/xxx 에 접속하여 일일히 기능에 대한 테스트를 해보는 것은
장애 발생을 일으킬 수 있고, 레거시 코드들을 리팩토링하는 과정에서 큰 오류가 발생할 수 있다.
그래서 웹 개발자의 경우 특히나 이러한 테스트 코드를 유용하게 사용하는 것이 중요한데,
테스팅 프레임워크의 유용한 도구들을 사용하여 테스트 코드를 작성하는 것은, 개발 과정 중 코드의 신뢰성을 높이고 빠르고 쉽게 구현하고 변경하는 과정에서 중요하다.

JUnit 5

JUnit 5 버전은 2017년 2월에 나왔고, 기존 레거시에서는 JUnit 4를 사용하는 회사가 많고, 새로 시작하는 프로젝트들 또는 개발에 관심있는 많은 회사에서는 버전 업 된 JUnit 5를 많이 사용한다고 한다.

JUnit 5의 기본 어노테이션은 다음과 같다. https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations

 

 

어노테이션 설명
@Test 테스트 메소드를 나타내는 어노테이션입니다. 필수로 작성되어야 합니다.
@BeforeEach 각 테스트 메소드 시작 전에 실행되어야 하는 메소드에 써줍니다.
@AfterEach 각 테스트 메소드 종료 후에 실행되어야 하는 메소드에 써줍니다.
@BeforeAll 테스트 시작 전에 실행되어야 하는 메소드에 써줍니다. (static 메소드여야만 함)
@AfterAll 테스트 종료 후에 실행되어야 하는 메소드에 써줍니다. (static 메소드여야만 함)
@Disabled 실행되지 않아야 하는 테스트 메소드에서 써줍니다.

 

JUnit 5 Assertions

JUnit 5에서 기본적으로 제공하는 Assertions, Assumptions 테스트 메서드들을 살펴보자.
Assertion이란, 한글로는 주장 의 의미를 담고 있으며 테스트가 원하는 결과를 제대로 반환하는지, 어떠한 에러가 발생하지는 않는지 확인할 때 사용하는 메서드이다.

메소드명 설명
fail 무조건 실패 (레거시에 사용하면 좋다.)
assertTrue 조건이 성공이면 True
assertFalse 조건이 실패면 True
assertNull 조건이 Null이면 True
aseertNotNull 조건이 Not Null이면 True
assertEquals(expected, actual) expected와 actual이 동일하면 True
assertArrayEquals 두 Array가 동일하면 True
assertIterableEquals 두 Iterable이 동일하면 True
assertLinesMatch 두 Stream이 동일하면 True
assertNotEquals expected와 actual이 다르면 True
assertSame 동일한 Object면 True
assertNotSame 다른 Object면 True
assertAll 여러 Assertion이 True면 True
assertThrows 예상한 에러가 발생하면 True
assertDoesNotThrow 에러가 발생하지 않으면 True
assertTimeout 테스트가 지정한 시간보다 오래 걸리지 않으면 True

지정한 시간보다 오래 걸려도 테스트가 끝날 때까지 대기
assertTimeoutPreemptively 테스트가 지정한 시간보다 오래 걸리지 않으면 True

지정한 시간보다 오래 걸린 경우 바로 테스트 종류

 

JUnit 5 Assumptions

Assumtion은 한글로는 추정의 의미를 담고 있다. 이는 메소드별 조건을 만족할 경우 진행시키고, 아닌 경우 스킵하는 메소드이다.
if 가정문이라고 생각해도 된다.

메소드명 설명
assumeTrue 테스트가 실패하면 에러 발생
assumeFalse 테스트가 성공하면 에러 발생
assumingThat(boolean, executable) 첫 번째 인자가 True면 두 번째 인자로 들어온 함수 실행

첫 번째 인자 값이 false 인 경우에도 테스트를 스킵하지 않고 다음 코드를 진행합니다.

 

Assertions vs Assumptions

Assertions는 개발자가 테스트하고 싶은 인자값을 넣어씅 ㄹ때 예상한 결과가 나오는지 테스트 해볼 경우 사용하며,
Assumptions는 개발자가 인자값을 정확히 모를 때, if 가정문 과 같은 용도로 사용하면 된다.


AssertJ

AssertJ를 이용한 테스트 메서드는 assertThat() 1개 이다.
AssertJ의 assertions는 assertThat("hello").isEqualTo("hello") 와 같이 메소드 체이닝 방식을 사용한다.

https://www.petrikainulainen.net/programming/testing/junit-5-tutorial-writing-assertions-with-assertj/


테스트 예제들



https://mkyong.com/junit5/junit-5-assertj-examples/ 을 참고하여, 테스트 코드 작성법에 대해 더 자세히 살펴보자.
위 블로그에 올라와있는 코드들을 통해 학습해보자.

String 테스트

"I am Mkyong!" 이라는 String 형 name 변수를 선언했다.
assertThat(name) 을 통해 이 name에 대한 테스트를 시작하게 된다.
assertThat() 이후로 나타난 메서드들을 쭉 나열하고 정리해보자.

  • as()
  • isEqualTo()
  • isEqualToIgnoringCase() -> 대소문자 구분을 안한 문자열과 동일한지
  • startswith(), endswith()
  • containsIgnoringCase() -> 대소문자 구분을 안한 문자열을 포함하는지

 

    // assert string
    @Test
    void test_string_ok() {

        String name = "I am Mkyong!";

        assertThat(name)
                .as("if failed display this msg!")
                .isEqualTo("I am Mkyong!")
                .isEqualToIgnoringCase("I AM mkyong!")
                .startsWith("I")
                .endsWith("!")
                .containsIgnoringCase("mkyong");

    }

 

List 테스트

List<String> 타입의 list 배열을 테스트하는 메서드이다,

  • hasSize() -> 배열의 크기
  • contains() -> 해당 요소(들)을 포함하는지
  • contains("", Index.atIndex()) -> 해당 인덱스에 해당 요소를 포함하는지
  • doesNotContain() -> 해당 요소를 포함 안하고 있는지

 

    // assert list
    @Test
    void test_list_ok() {

        List<String> list = Arrays.asList("Java", "Rust", "Clojure");

        assertThat(list)
                .hasSize(3)
                .contains("Java", "Clojure")
                .contains("Java", Index.atIndex(0))
                .contains("Rust", Index.atIndex(1))
                .contains("Clojure", Index.atIndex(2))
                .doesNotContain("Node JS");

    }

 

Map 테스트

Map<String, Object> 타입을 테스트하는 메서드이다.
map에는 key(name), value(mkyong)을 추가한 상태이다.

  • hasSize() -> 맵의 요소 크기
  • extractingByKey("key", as(InstanceOfAssertFactories.STRING)) -> 해당 키로 value를 추출(STRING 형태로)
    • isEqaulToIgnoiringCase() -> value가 대소문자 구분하지 않는 해당 문자열과 동일한지
    • startswith() -> value가 해당 문자열로 시작하는지

 

    // assert map
    @Test
    void test_map_ok() {

        Map<String, Object> map = new HashMap<>();
        map.put("name", "mkyong");

        assertThat(map)
                .hasSize(1)
                .extractingByKey("name", as(InstanceOfAssertFactories.STRING))
                .isEqualToIgnoringCase("mkyong")
                .startsWith("mkyong");

        assertThat(map).extracting("name")
                .isEqualTo("mkyong");

        Map<String, Object> map2 = new HashMap<>();
        map2.put("number", 999);

        assertThat(map2)
                .hasSize(1)
                .extractingByKey("number", as(InstanceOfAssertFactories.INTEGER))
                .isEqualTo(999);

    }

 

Exception 테스트

이번에는 예외처리에 대한 테스트 메서드이다.

첫번째는 1을 0으로 나누는 divide() 메서드를 실행했을 때 던져니는 예외에 대해서 assertThatThrownBy() 메서드를 통해 테스트를 진행한다.

  • isInstanceOf(ArithmeticException.class) -> ArithmeticException 클래스의 인스턴스인지
  • hasMessageContaining() -> 해당 예외 메시지가 해당 문자열을 포함하는지
  • hasMessage() -> 해당 문자열의 예외 메시지를 갖고 있는지


두번째는 2주차 Study용 테스트 코드에도 나와있었던 IndexOutOfBoundsException 예외 처리 테스트 코드이다.

    // assert exception
    @Test
    void test_exception_ok() {

        assertThatThrownBy(() -> divide(1, 0))
                .isInstanceOf(ArithmeticException.class)
                .hasMessageContaining("zero")
                .hasMessage("/ by zero");

        assertThatThrownBy(() -> {
            List<String> list = Arrays.asList("one", "two");
            list.get(2);
        })
                .isInstanceOf(IndexOutOfBoundsException.class)
                .hasMessageContaining("Index 2 out of bounds");

    }

    int divide(int input, int divide) {
        return input / divide;
    }
728x90
반응형
Comments