JUnit5 & AssertJ & MockMVC 기본 사용법

5tr1ker·2023년 3월 6일
1
post-thumbnail

JUnit5 이란?

자바 개발자가 가장 많이 사용하는 테스팅 기반 프레임워크입니다.
JUnit5는 자바 8 부터 사용이 가능하며 테스트 작성자를 위한 API 모듈과 테스트 실행을 위한 API가 분리되어 있습니다.

JUnit5 어노테이션

  • @Test : 테스트 Method임을 선언합니다.
  • @ParameterizedTest : 매개변수를 받는 테스트입니다.
  • @RepeatedTest : 반복되는 테스트를 작성합니다.
  • @TestFactory : @Test로 선언된 정적 테스트가 아닌 동적으로 테스트를 사용합니다.
  • @TestInstance : 테스트 클래스의 생명주기를 설정합니다.
  • @TestTemplate : 공급자에 의해 여러 번 호출될 수 있도록 설계된 테스트 케이스 템플릿임을 알립니다.
  • @TestMethodOrder : 테스트 메소드 실행 순서를 구성하는데 사용됩니다.
    -> MethodOrderer.OrderAnnotation.class 를 인자로 전달하여 @Order 로 순서 지정합니다.
  • @Order : 테스트 메소드 실행 순서를 구성하는데 사용됩니다.
  • @DisplayName : 테스트 클래스 , 메소드의 사용자 정의 이름을 선언할 때 사용됩니다.
  • @DisplayNameGeneration : 이름 생성기를 선언합니다. ( 예시로 공백을 _ 문자로 치환 )
  • @BeforeEach : 모든 테스트 실행 전에 실행할 테스트에 사용합니다.
  • @AfterEach : 모든 테스트 실행 후에 실행할 테스트에 사용합니다.
  • @BeforeAll : 현재 클래스를 실행하기 전 제일 먼저 실행할 테스트에 사용됩니다. static으로 선언해야 합니다.
  • @AfterAll : 현재 클래스 종료 후 해당 테스트를 실행합니다. static으로 선언해야 합니다.
  • @Nested : 클래스를 정적이 아닌 중첩 테스트 클래스임을 나타냅니다.
  • @Tag : 클래스 또는 메소드 레벨에서 태그를 선언할 때 사용합니다.
  • @Disable : 이 클래스나 테스트를 사용하지 않습니다.
  • @Timeout : 테스트 실행 시간을 선언 후 초과하면 실패합니다.
  • @ExtendWith : 확장을 선언적으로 등록할 때 사용합니다.
  • @RegisterExtension : 필드를 통해 프로그래밍 방식으로 확장을 등록할 때 사용합니다.
  • @TempDir : 필드 주입 또는 매개변수 주입을 통해 임시 디렉토리를 만들때 사용합니다.

Jupiter API : assert 메서드

org.junit.jupiter.api.Assertions 클래스는 값 검증을 위한 assert로 시작하는 static 메서드를 제공하고 있습니다.

하단의 Object 의 값은 비즈니스 모델의 함수를 호출하며 반환 값을 이용해 테스트를 진행할 수 있습니다.

  • assertEquals(Object expected, Object actual) : expected 의 값이 actual 일 때 성공합니다.
  • assertNotEquals(Object unexpected, Object actual) : unpxpected 의 값이 actual 이 아닐 때 성공합니다.
  • assertTrue(boolean condition) : condition 값이 True 값일 때 성공합니다.
  • assertFalse(boolean condition) : condition 값이 False 값일 때 성공합니다.
  • assertNull(Object actual) : actual 의 값이 Null 일 때 성공합니다.
  • assertNotNull(Object actual) : actual 의 값이 Null 이 아닐 때 성공합니다.
  • assertThrows(ArithmeticException.class, () -> 메소드) : 해당 코드가 특정 예외를 발생하면 성공합니다.
  • assertDoesNotThrow(() -> 메소드) : 어떠한 예외도 반환하지 않았다면 성공합니다.

더 많은 Assertions 메서드를 보고싶을 때 해당 링크 를 들어가면 더 많은 메서드를 활용할 수 있습니다. 활용 블로그

MockMVC란?

사전적 의미로 '테스트를 위해 만든 모형'을 의미하고, 테스트를 위해 실제 객체와 비슷한 모의 객체를 만드는 것을 모킹(Mocking) , 모킹한 객체를 메모리에서 얻어내는 과정을 목업 ( Mock-up ) 이라고 합니다. 이러한 복잡한 객체를 테스트하기 위해 실제 객체와 비슷한 가짜 객체를 만들어 테스트에 필요한 기능만 가지게 하면 테스트가 쉬워집니다. 또한 복잡한 의존성을 가지고 있을 때, 모킹한 객체를 이용하면 의존성을 단절시킬 수 있어 쉽게 테스트할 수 있습니다.

MockMVC : 컨트롤러 테스트

MockMVC 는 Controller 테스트를 위한 객체이며 perform() 메소드를 지원해 해당 메소드를 통해 Controller 호출 테스트를 할 수 있습니다.

@AutoConfigureMockMvc : 무조건 테스트 클래스 상단에 명시해 주셔야합니다.
또한 gradle로 설치시 다음과 같은 의존성이 있어야 합니다.

testCompile("org.springframework.boot:spring-boot-starter-test")
  • MockMVC 사용 예제

	@InjectMocks
	private Controller userController;

	MockMvc mockMvc;
    
	@BeforeEach
    public void init() {
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
    }

mvc.perform(MockMvcRequestBuilders
	.get("API 주소")
    .accept(MediaType.APPLICATION_JSON))
    .param("number1", "1") // .params("number1", "1" , "number2" , "2")
	.header("Authorization", result.getAccessToken()))
    .andExpect(status().is3xxRedirection());

MockMVC 메서드

  • perform : 요청을 전송하는 역할을 합니다. 결과로 ResultActions 객체를 반환하며 검증을 위한 andExpect() 메서드를 제공합니다.
  • get : HTTP 메서드를 결정할 수 있으며 인자로 url 경로를 삽입합니다. ( put() , delete() , patch() , post() )
  • accept : JSON 형식의 데이터만 허용합니다.
  • contentType : ContentType 을 지정합니다.
    - ex ) json 데이터일 경우 MediaType.APPLICATION_JSON
  • param : 키,값 파라미터를 전달합니다. 여러 개일 경우 params 을 사용합니다.
  • header : 키,값으로 헤더를 지정할 수 있습니다.
  • andExpect : 응답을 검증합니다.
    - status : 상태코드를 나타냅니다.
    - isOk() , is2xxRedirection() : 200 응답
    - isCreated() , is2xxRedirection() : 201 응답
    - isNotFound() , is4xxRedirection() : 404 응답
    - isMethodNotAllowed() , is4xxRedirection() : 405 응답
    - isInternalServerError() , is5xxRedirection() : 500 응답
    - is(int statius) : Http Status 상태 코드를 직접 할당합니다.
    • view : 리턴되는 뷰 이름을 검증합니다.
      • name("aAa") : 뷰의 이름이 aAa 인지 검증합니다.
    • content : 응답 반환값 대한 검증을 합니다.
      • string(String content) : 괄호안에 문자를 포함하는지 확인합니다.
    • redirect : 리다이렉트 응답을 검증합니다. 인자로 리다이렉트 되는 경로를 기입합니다.
      ex ) redirectUrl("/naver") -> '/naver' 로 리다이렉트 되었는가?
  • andDo(print()) : Test 응답 결과에 대한 모든 정보를 출력합니다.

MockMVC 결과 받기

만약 결과를 받고 싶다 하면 getResponse() 메서드를 활용할 수 있는데 코드는 다음과 같습니다.

ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.get("/user"));

MvcResult mvcResult = resultActions.andExpect(status().isOk()).andReturn();
String str = mvcResult.getResponse().getContentAsString();

우선 API 호출을 한 결과를 resultActions 변수에 받고 그 결과를 .andExpect 메서드를 활용해 200 결과인지 확인한 다음 getResponse() 메서드를 활용해 결과값을 가져와 getContentAsString(); 로 문자열로 파싱합니다.

참고 : https://insight-bgh.tistory.com/507
https://scshim.tistory.com/317

AssertJ 란?

AssertJ는 assertion을 제공하는 자바 라이브러리로 에러 메세지와 테스트 코드의 가독성을 높여주는 라이브러리입니다.
JUnit5에서 제공하는 Assertions의 assert는 인자 순서가 헷갈릴 수 있지만, AssertJ의 Assertions은 가독성이 좋고 헷갈리지 않습니다.

예시 )

assertEquals(expected, actual);  // JUnit5
assertThat(actual).isEqualTo(expected); // AssertJ
  • AssertJ를 사용하여 메소드 체이닝을 활용해 좀 더 깔끔하고 읽기 쉬운 테스트 코드를 작성합니다.
  • 개발자가 테스트를 하면서 필요한 모든 메서드를 제공합니다.

의존성 : testImplementation group: 'org.assertj', name: 'assertj-core', version: '3.21.0'

문자열 테스트 케이스


assertThat("문자열..") // 해당 문자열의 결과값을 테스트 합니다.
			.isNotEmpty() // Null 값이 아니며
           	.contains("Nice") // "Nice"를 포함하고
            .contains("world") // "world"도 포함하고
            .doesNotContain("ZZZ") // "ZZZ"는 포함하지 않으며
            .startsWith("Hell") // "Hell"로 시작하고
            .endsWith("u.") // "u."로 끝나며
            .isEqualTo("문자열.."); // "문자열.." 과 일치합니다.

// 정리
- isNotEmpty() // 비어있지 않을 경우
- isNotNull() // Null 이 아니면
- startsWith("문자열") // "문자열"로 시작하면
- endsWith("s.") // "s." 로 끝나면
- doesNotContain("aaa")  // "aaa"는 포함하지 않으며
- contains("Java")   // "Java"를 포함하고
- contains("Success")  // "Success"도 포함하며
- isEqualTo("문자열") // "문자열" 을 equals 메서드로 비교시 true이면
- isNotEqualTo("문자열") // "문자열" 을 equals 메서드로 비교시 false이면
- isSameAs("문자열") // 주소값이 같은 문자열 일경우
- isNotSameAs("문자열") // 주소값이 다른 문자열 일경우
- isInstanceOf(String.class) // String 클래스이고
- isInstanceOf(CharSequence.class) // String 이 구현한 CharSequence 인터페이스이기도 하고
- isNotInstanceOf(Character.class) // Character 클래스는 아니며

정수형 테스트 케이스

assertThat(100) // 정수형 100 을 테스트합니다.
			.isPositive()	// 양수이며
    		.isGreaterThan(3) // 3보다 크며
            .isLessThan(4) // 4보다 작습니다
            .isEqualTo(3, offset(1d)) // 오프셋 1 기준으로 3과 같고
            .isEqualTo(3.1, offset(0.1d)) // 오프셋 0.1 기준으로 3.1과 같으며
            .isEqualTo(10 , offset(2d)) // 오프셋 2 기준으로 10과 같으며
            .isEqualTo(3.14); // 오프셋 없이는 3.14와 같습니다
            
//정리
.isBetween(8.67, 9d) // 8.67 이상 9 이하이고
                .isStrictlyBetween(8.66, 9d) // 8.66 초과 9 미만이며
                
                .isCloseTo(8, offset(0.67d)) // 8과의 오차범위가 0.67 이내이고
                .isCloseTo(8, withPercentage(10)) // 8과의 오차범위가 10퍼센트 이내이며
                
                .isNotCloseTo(8, offset(0.66d)) // 8과의 오차범위가 0.66 초과이고
                .isNotCloseTo(8, withPercentage(1)) // 8과의 오차범위가 1퍼센트 초과이며
                
                .isGreaterThan(8)   // 8보다 크고
                .isLessThan(9)  // 9보다 작으며
                
                .isPositive()   // 양수이고
                .isNotNegative() // 음수가 아니고
                .isNotZero() // 0이 아니며
                
                .isFinite() // 유한한 숫자이고
                .isNotNaN() // NAN 이 아니며
                
                .isEqualTo(8, offset(1d))  // 8과의 차이가 오차범위 1 이내이고
                .isEqualTo(8.6, offset(0.1d))  // 8.6과의 차이가 오차범위 0.1 이내이며
                .isEqualTo(8.67)   // 오프셋 없이는 8.67와 같습니다
                
                .toString();

그 외에 더 많은 검증 메서드를 활용하고 싶다음 다음 링크 를 참고해주세요.

논리식 테스트 케이스

.isTrue() : 해당 결과가 참이면 통과합니다.
.isFalse() : 해당 결과가 거짓이면 통과합니다.
.isNull() : 해당 결과가 Null이면 통과합니다.
.isNotNull() : 해당 결과가 Null이 아니면 통과합니다.

  • 사용 예시
assertThat(lotto.stream().allMatch(v -> v >= 1 && v <= 45)).isTrue();
// lotto 는 List형 변수이며 해당 요소가 모두 1 이상 45 이하이면 테스트 통과

Test Fail 메세지

Test Fail 메세지는 테스트 결과 메세지를 튜닝하는 기능을 갖고있습니다.

에러 메세지는 as 메서드로 지정할 수 있습니다. 단 , as 메서드는 assertions이 수행되기전에 사용해야합니다.

assertThat(500) // 500을 테스트 합니다.
	.as("[테스트 : %d ] " , 500)
    .isEqualTo(100);	// 100 과 일치해야 합니다.

메세지 결과 : [테스트 : 500] expected:<100> but was:<33>

Filtering assertions

filtering assertions는 람다 또는 배열 , 링크드리스트에 적용할 수 있습니다.
해당 배열에 값이 들어있는지 필터링하는 기능입니다.

// 단순 리스트
assertThat(리스트).filteredOn(data -> data.equals("문자열1")).containsOnly("문자열2" , "문자열3");

// 클래스 리스트
assertThat(리스트).filteredOn(data -> data.getName().equals("문자열1")).containsOnly("문자열2" , "문자열3");

assertThat(list).filteredOn("age", notIn(22)).containsOnly(park, lee);

// 연쇄적
assertThat(members)
                .filteredOn("type", ADMIN)
                .filteredOn(m -> m.getAge() > 20)
                .containsOnly(james);

filteredOn 메서드는 연쇄적으로 진행할 수 있습니다!!

배열 요소중에 "문자열1" 과 일치하는 값 중에서 "문자열2" 혹은 "문자열3"을 포함하고 있다면 테스트 성공이라는 의미를 갖고 있습니다. containsOnly 안에는 여러 문자열을 포함할 수 있습니다.

3번째에 filteredOn 메서드 두번째 인자로 notIn이 들어간것을 볼 수 있는데, 이것은 age 필드가 22가 아닌 객체들을 검증하는 것 입니다. 사용할 수 있는 함수는 다음과 같습니다.

  • not() : 해당 필드에 해당 값이 아닌 객체
  • in() : 해당 필드에 해당 값이 들어간 객체
  • notIn() : 해당 필드에 해당 값이 들어가 있지 않은 객체

Assertions on Extracted

리스트에서 특정 필드만 뽑아 테스트할 수 있습니다. 특정 필드는 클래스의 필드를 의미합니다.

assertThat(클래스 배열).extracting("필드 명").contains("문자열1" , "문자열2").doesNotContain("문자열3" , "문자열4");

클래스 리스트안에 '필드 명' 필드중에 "문자열1" 혹은 "문자열2" 를 포함하고 있으며 , "문자열3" , "문자열4" 를 포함하고 있지 않다면 성공합니다.

추가로 강하게 타입을 명시하고 싶으면 extracting("필드 명" , String.class) 로 클래스의 이름을 명시할 수 있습니다. 해당 "필드 명"이 문자열 이라는 것을 명시하는 것입니다.

여러 필드를 검사하고 싶은 경우 tuple를 사용해주세요.

assertThat(list).extracting("name", "age") // name 과 age 필드를 탐색
            .contains(tuple("Kim", 22),
                    tuple("Park", 25),
                    tuple("Lee", 22),
                    tuple("Amy", 25),
                    tuple("Jack",22))

Soft assertions

soft assertions을 사용하면 모든 assertions을 실행한 후 실패 내역만 확인할 수 있습니다.

이는 테스트 중 assertThat() 하나가 실패하면 해당 테스트 자체가 중단되는데, soft assertions을 사용하면 실패 내역만 볼 수 있습니다.

SoftAssertions softly = new SoftAssertions();
softly.assertThat(num1).as("num1 is %d", num1).isEqualTo(5);
softly.assertThat(num2).as("num2 is %d", num2).isEqualTo(6);
softly.assertThat(str).as("str is %s", str).isEqualTo("hi");
softly.assertAll();

한 테스트에 여러 테스트를 진행하여 실패한 테스트만 보여주며 모두 성공했을 시엔 통과합니다.

File assertions

파일에 대한 테스트를 제공하며 해당 데이터가 파일인지 , 존재하는지 테스트를 할 수 있습니다.

File file = new File(fileName);

assertThat(file).exists().isFile().isRelative();

assertThat(contentOf(file))
            .startsWith("You")
            .contains("Know Nothing")
            .endsWith("Jon Snow");
  • exists() : 파일이 존재하는지 확인합니다.
  • isFile() : 파일이 맞는지 확인합니다.
  • isRelative() : 상대 경로인지 확인합니다.
  • contentOf() : 파일의 내용을 가져와 문자열로 테스트할 수 있습니다.

Exception assertions

assertThatThrownBy() 메서드를 활용해 예외 처리를 할 수 있습니다.

AssertJ에서는 다음과 같은 함수가 대표적입니다. 각각의 함수는 해당 예외를 다룹니다.

  • assertThatNullPointerException

  • assertThatIllegalArgumentException

  • assertThatIllegalStateException

  • assertThatIOException

  • 예시

assertThatNullPointerException().isThrownBy(() -> { 메서드 })
	.withMessage("%s" , "메세지")
    .withMessageContaining("nu");
  • 그게 아니라면 예외 클래스를 직접 설정할 수 있습니다.
assertThatExceptionOfType(IOException.class).isThrownBy(() -> { throw new IOException("boom!"); })
                .withMessage("%s!", "boom")
                .withMessageContaining("boom");
  • assertThatExceptionOfType() : 예외 탐지할 메서드입니다.
  • isThrownBy() : 예외가 발생하는 메서드입니다.
  • withMessageContaining("문자열") : 예외 메세지가 "문자열" 을 포함하면 성공
  • withMessage("문자열") : 예외 메세지가 "문자열" 일경우 성공합니다.

방법 2

assertThatThrownBy(() -> { throw new Exception("boom!"); })
            .isInstanceOf(Exception.class)
            .hasMessageContaining("boom");
  • assertThatThrownBy() : 예외가 발생하는 메서드
  • isInstanceOf() : 발생한 예외 클래스
  • hasMessageContaining() : 해당 메세지를 포함하고 있어야 합니다.

그 외에도 여러 메서드가 존재합니다. 이름이 직관적이니 설명을 굳이 하지 않아도 바로 사용할 수 있습니다.

BDD 스타일로 처리하기

// given
    final LottoNumberGenerator lottoNumberGenerator = new LottoNumberGenerator();
    final int price = 2000;

    // when
    final RuntimeException exception = assertThrows(RuntimeException.class, () -> lottoNumberGenerator.generate(price));

    // then
    assertThat(exception.getMessage()).isEqualTo("올바른 금액이 아닙니다.");
    
    // 혹은 어떤 클래스 예외인지 확인 하려면
assertThat(thrown).isInstanceOf(UnauthorizedException.class).hasMessageContaining("Invalid access token");

다음처럼 RuntimeException 예외가 발생했을 때 when 단계에서 assertThrows() 를 활용해 변수를 리턴 받아 then 단계에서 assertThat() 메서드로 처리할 수 있습니다.

예외가 발생하지 않을 경우

// WHEN
Throwable thrown = catchThrowable(() -> { throw new Exception("boom!"); });

// THEN
assertThat(thrown).doesNotThrowAnyException();
  • doesNotThrowAnyException() : 어떤 예외도 발생하지 않아야 합니다.

BDD 스타일

BDD 스타일은 Given , when , then 으로 이루어진 스타일로 조금 더 가독성 있는 코드를 작성할 수 있습니다.

자세한 내용은 하단 링크의 블로그에서 자세하게 다룹니다.

BDD 스타일에 대해 자세하게 알기

참고

참고 블로그 1 : https://pjh3749.tistory.com/241
참고 블로그 2 : https://hseungyeon.tistory.com/328
참고 블로그 3 : https://steady-coding.tistory.com/351
참고 블로그 4 : https://sun-22.tistory.com/86

profile
https://github.com/5tr1ker

0개의 댓글