티스토리 뷰

반응형

테스트 코드는 작성한 코드가 의도대로 동작되는지, 예상하지 못한 문제가 없는지 확인하기 위해 작성하는 코드이다.

테스트 코드는 개발 공부를 할 때 미루는 경우가 많지만, 유지보수와 기능 작동에 신경 쓸 필요 없는 환경이 되므로 공부해보는 걸 추천한다.

 

테스트 코드는 기본적으로 프로젝트에서 src > test에 존재하고 있다.

 

다양한 패턴이 있는데, 그 중에서도 given-when-then 패턴을 알아보자.

 

given-when-then 패턴

given-when-then 패턴은 테스트 코드를 세 단계로 구분해서 작성하는 방식이다.

1. given - 테스트 실행 준비 단계

2. when - 테스트 진행 단계

3. then - 테스트 결과 검증 단계.

 

예를 들어 새로운 변수를 저장하는 코드를 테스트한다면 다음과 같다.

@DisplayName("변수 저장")
@Test
public void saveXTest() {
	// given : 저장하기 위한 준비
    final String name = 'cat";
    final int cost = 1000;
    final X cat = new X(name, cost);
    
    // when : 실제 저장
    final long saveId = xSave.save(cat);
    
    // then : 실제로 잘 저장되었는지 검증
    final X xSave = xSave.findById(saveID).get();
    assertThat(xSave.getName()).isEqualTo(name);
    assertThat(xSave.getCost()).isEqualTo(cost);

 

 

일단 이렇게 나뉘어져 있다고만 알고 넘어가보자.

 

스프링 부트3에서의 테스트

 

스프링 부트는 애플리케이션 테스팅 도구와 에너테이션을 제공한다.

spring-boot-starter-test 스타터에 테스트를 위한 도구들이 모여있다.

 

스타터 테스트 목록

JUnit : 자바 언어용 단위 테스트 프레임워크

Spring Test & Spring Boot Test : 스프링 부트 애플리케이션을 위한 통합 테스트 지원

AssertJ : 어설션(검증문)을 작성하는 데 사용되는 라이브러리

Hamcrest : 표현식을 이해하기 쉽게 만드는데 사용되는 Metcher 라이브러리

Mockito : 테스트에 사용할 가짜 객체인 목 객체를 쉽게 만들고, 관리하고, 검증할 수 있게 지원하는 테스트 프레임워크

JSONassert : JSON용 어설션 라이브러리

JsonPath : JSON 데이터에서 특정 데이터를 선택하고 검색하기 위한 라이브러리

 

이 중에서 JUnit과 AssertJ를 가장 많이 사용한다.

 

JUnit은?

 

JUnit은 자바 언어를 위한 단위 테스트 프레임워크이다.

단위 테스트는 작성한 코드가 의도대로 작동하는지 작은 단위로 검증하는 것을 의미한다.

이때의 단위는 보통 메서드가 된다(파이썬으로 따지면 def). JUnit을 사용하면 단위 테스트를 작성하고 테스트한 데 도움을 준다.

 

JUnit의 특징

테스트 방식을 구분할 수 있는 에너테이션 제공

@Test 에너테이션으로 메서드를 호출할 때마다 새 인스턴스를 생성, 독립 테스트 가능

예상 결과를 검증하는 어셜선 메서드 제공

사용 방법이 단순, 테스트 코드 작성 시간이 적음

자동 실행, 자체 결과를 확인하고 즉각적인 피드백을 제공

 

JUnit을 실제로 사용해봅시다.

src > test > java 폴더에 JUnitTest.javs 파일을 생성

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class JUnitTest {
    @DisplayName("1 + 2는 3이다") //테스트 이름
    @Test // 테스트 메서드
    public  void junitTest(){
        int a=1;
        int b=2;
        int sum = 3;

        Assertions.assertEquals(sum,a+b);  //값이 같은지 확인
    }
}

 

 

@DisplayName 에너테이션은 테스트 이름을 명시

@Test 에너테이션을 붙인 메서드는 테스트를 수행하는 메서드

JUnit은 테스트끼리 영향을 주지 않도록 각 테스트를 실행할 때마다 테스트를 위한 실행 객체를 만들고, 테스트가 종료되면 실행 객체를 삭제한다.

 

JUnit에서 제공하는 검증 메서드인 assertEquals()로 a+b와 sum의 값이 같은지 확인한다.

첫 번째 인수에는 기대하는 값, 두 번째 인수에는 검증할 값을 넣어준다.

 

실행 동작 확인은 JUnitTest.java 파일을 우클릭해서, Run JUnitTest를 누르면 된다.

테스트가 끝나면 콘솔창에 테스트 결과가 출력된다.

성공 여부나 실행 시간 등의 정보를확인하기 위해서는 Run 프롬프트에서 체크 아이콘인 Show Passed를 클릭하면 된다.

 

 만약 테스트가 실패한다면 어떻게 될까?

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class JUnitTest {
    @DisplayName("1 + 2는 3이다") //테스트 이름
    @Test // 테스트 메서드
    public  void junitTest(){
        int a=1;
        int b=2;
        int sum = 3;

        Assertions.assertEquals(sum,a+b);  //값이 같은지 확인
    }

    @DisplayName("1 + 3은 4이다") //테스트 이름
    @Test // 테스트 메서드
    public  void junitFailTest(){
        int a=1;
        int b=3;
        int sum = 3;

        Assertions.assertEquals(sum,a+b);  //값이 같은지 확인
    }
}

 

다음과 같이 테스트가 실패했다고 하며 실제로 받은 값과 비교를 해준다.

Expected :3
Actual   :4

 

JUnit은 테스트 케이스가 하나라도 실패하면 전체 테스트를 실패한 것으로 보여준다.

 

JUnit의 애너테이션들

이제 위의 코드에서 junitFailedTest() 메서드는 삭제하고, JUnitCylceTest.java 파일을 만들어봅시다.

테스트는 애너테이션에 따라 실행 순서가 정해집니다. 

import org.junit.jupiter.api.*;

public class JUnitCylceTest {
    @BeforeAll // 모든 테스트를 실행하기 전에 1회 실행하는 메서드는 static으로 선언
    static void beforeAll() {
        System.out.println("@BeforeAll");
    }

    @BeforeEach // 각 테스트를 시작하기 전마다 실행
    public void beforeEach() {
        System.out.println("@BeforeEach");
    }

    @Test
    public void test1() {
        System.out.println("test1");
    }

    @Test
    public void test2() {
        System.out.println("test2");
    }

    @Test
    public void test3() {
        System.out.println("test3");
    }

    @AfterAll // 전체 테스트를 마치고 종료하기 전에 1회 실행하는 메서드는 static으로 선언
    static void afterAll() {
        System.out.println("@AfterAll");
    }

    @AfterEach // 테스트 케이스를 종료하기 전마다 실행
    public void afterEach() {
        System.out.println("@AfterEach");
    }
}

 

@BeforeAll 애너테이션

전체 테스트를 시작힉 전에 처음으로 한 번만 실행

데이터베이스를 연결하거나 테스트 환경을 초기화할 때 사용

 

메서드를 static으로 선언해야 한다.

 

@BeforeEach 애너테이션

테스트 케이스를 시작하기 전에 매번 실행.

예를 들어 테스트 메서드에서 사용하는 객체를 초기화하거나 테스트에 필요한 값을 미리 넣을 때 사용

각 인스턴스에 대해 메서드를 호출해야 하므로 메서드는 static이 아니어야 한다.

 

@AfterAll 애너테이션

전체 테스트를 마치고 종료하기 전에 한 번만 실행

데이터베이스 연결 종료, 자원 해제 등

전체 테스트 실행 주기에서 한번만 호출해야 함으로 역시 메서드는 static으로 선언

 

@AfterEach 애너테이션

각 테스트 케이스를 종료하기 전 매번 실행.

테스트 이후에 특정 데이터를 삭제해야 하는 경우 사용

@BeforeEach와 마찬가지로 static이 아니어야 함.

 

 

@BeforeAll ->  @BeforeEach -> JUnit 테스트 실행 -> @AfterEach -> @AfterAll

순서

실제 실행해본다면 다음과 같이 찍힌다.

@BeforeAll
@BeforeEach
test1
@AfterEach
@BeforeEach
test2
@AfterEach
@BeforeEach
test3
@AfterEach
@AfterAll

 

즉, AfterEach와 BeforEach는 모든 테스트가 진행되는동안 찍히고,

@BeforeAll과 @AfterAll은 한번만 찍힌다.

 

이제 JUnit은 마치고 다음으로 넘어가 봅시다.

 

AssertJ 사용하기

AssertJ는 검증문의 가독성을 높여주는 라이브러리입니다.

앞서 작성한 테스트 코드의 Assertion은 기댓값과 실제 비교값을 명시하지 않으므로 비교 대상이 헷갈린다.

 

Assertions.asserstEquals(sum, a+b)

 

그러나 AssertJ는 다음과 같다.

 

assertThat(a+b).isEqualTo(sum)

 

앞이 실제 비교값, 뒤에가 기대값이 된다. 이러면 코드를 읽는 사람이 헷갈리지 않는다.

AssertJ에는 다음과 같이 메서드 이름들이 존재한다.

isEqualTo(A) A 값과 같은지 검증
isNotEqualTo(A) A 값과 다른지 검증
contains(A) A 값을 포함하는지 검증
doesNotContain(A) A 값을 포함하지 않는지 검증
startsWith(A) 접두사가 A인지 검증
endsWith(A) 접미사가 A인지 검증
isEmpty() 비어 있는 값인지 검증
isNotEmpty() 비어 있지 않은 값인지 검증
isPositive() 양수인지 검증
isNegative() 음수인지 검증
isGreaterThan(1) 1보다 큰 값인지 검증
isLessThan(1) 1보다 작은 값인지 검증

 

테스트 코드 작성 연습 및 문제 풀어보기

String으로 선언한 변수 3개가 있습니다. 여기에서 세 변수 모두 NULL이 아니며 name1과 name2는 같은 값을 가지고, name3는 다른 나머지 두 변수와 다른 값을 가지는 데, 이를 검증하는 테스트를 작성하시오.

@test
public void junitTest() {
    String name1 = "홍길동";
    String name2 = "홍길동";
    String name3 = "홍길은";
    
    // 모든 변수가 null이 아닌지 확인
    // name1과 name2가 같은지 확인
    // name1과 name3가 다른지 확인

}

 

정답

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Java6Assertions.assertThat;

public class JUnitQuiz {

    @Test
    public  void junitTest(){
        String name1 = "홍길동";
        String name2 = "홍길동";
        String name3 = "홍길은";

        assertThat(name1).isNotNull();
        assertThat(name2).isNotNull();
        assertThat(name3).isNotNull();

        assertThat(name1).isEqualTo(name2);

        assertThat(name1).isNotEqualTo(name3);
    }
}

 

 

JUnitCycleQuiz.java 파일을 추가해준다.

테스트를 시작하기 전에 Hello를 출력하고, 끝마치고 Bye를 출력해봅시다.

기본 코드는 다음과 같습니다.

import org.junit.jupiter.api.Test;

public class JUnitCycleQuiz {
    @Test
    public void junitQuiz(){
        System.out.println("첫번째 테스트입니다");
    }
    
    @Test
    public void junitQuiz2(){
        System.out.println("두번째 테스트입니다");
    }
}

 

정답

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.*;

public class JUnitCycleQuiz {

    @BeforeEach
    public void printHello(){
        System.out.println("Hello!");
    }

    @Test
    public void junitQuiz(){
        System.out.println("첫번째 테스트입니다");
    }

    @Test
    public void junitQuiz2(){
        System.out.println("두번째 테스트입니다");
    }

    @AfterAll
    static void printBye(){
        System.out.println("Bye!");
    }

}

 

 

제대로 테스트 코드 작성해보기

이제까지 배운 내용을 바탕으로 제대로 테스트 코드를 작성해봅시다.

 

TestController.java 파일을 열고 클래스 이름 위에 마우스 커서를 놓고 클릭한 다음, Alt+Enter를 누르면 Create Test가 나타납니다.

 

그 후에 Create Test 창이 열리고 OK를 누르면 TestControllerTest.java 파일이 test/java/패키지 아래에 생성됩니다.

 

생성된 파일에 다음과 같이 작성합니다.

 

package me.shinsunyoung.springbottdeveloper;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;

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

@SpringBootTest
@AutoConfigureMockMvc
class TestControllerTest {
    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private MemberRepository memberRepository;

    @BeforeEach // 테스트 실행 전 실행하는 메서드
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @AfterEach // 테스트 실행 후 실행하는 메서드
    public void cleanUp() {
        memberRepository.deleteAll();
    }
}

 

@SpringBootTest는 메인 애플리케이션 클래스에 추가되는 에너테이션인

@SpringBootApplication이 있는 클래스를 찾고 그 클래스에 포함되어 있는 빈을 찾은 다음 테스트용 애플리케이션 컨텍스트라는 걸 만들게 됩니다.

 

@AutoConfigureMockMvc는 MockMvc를 생성하고 자동으로 구성하는 에너테이션입니다.

MockMvc는 애플리케이션을 서버에 배포하지 않고도 테스트용 MVC 환경을 만들어 요청 및 전송, 응답 기능을 제공합니다.

즉, 컨트롤러를 테스트를 할 때 사용되는 클래스입니다.

 

@BeforEach

테스트를 실행하기 전에 실행하는 메서드에 적용하는 에너테이션입니다. 여기서는 MockMvcSetUp() 메서드를 실행해 MockMvc를 설정해줍니다.

 

@AfterEach

테스트를 실행한 이후에 실행하는 메서드에 적용하는 에너테이션입니다. 여기서는 cleanUp() 메서드를 실행해 member 테이블에 있는 데이터들을 모두 삭제해줍니다.

 

모두 작성하고 나면 테스트 코드의 작성이 완료된 겁니다. 그럼 이제 TestController의 로직을 테스트하는 코드를 작성하겠습니다.

@DisplayName("getAllMembers: 아티클 조회에 성공한다.")
    @Test
    public void getAllMembers() throws Exception {
        // given
        final String url = "/test";
        Member savedMember = memberRepository.save(new Member(1L, "홍길동"));

        // when
        final ResultActions result = mockMvc.perform(get(url) // 1
                .accept(MediaType.APPLICATION_JSON)); // 2

        // then
        result
                .andExpect(status().isOk()) // 3
                .andExpect(jsonPath("$[0].id").value(savedMember.getId()))
                .andExpect(jsonPath("$[0].name").value(savedMember.getName()));
    }

 

코드를 다 작성하면 new Member에 빨간줄이 생긴다.

lombok 플러그인이 없어서 일어나는 일이다.

 

Settings -> Plugins -> Marketplace 에서 lombok를 검색한 뒤 다운로드하고 인텔리제이를 다시 시작한다.

 

이 테스트에는 Given-When-Then 패턴이 적용되어 있다.

의도한 테스트는 다음과 같다.

 

Given - 멤버를 지정

When - 멤버 리스트를 조회하는 API를 호출

Then - 응답 코드가 200이고, 반환받은 값 중에 0번째 요소의 id와 name이 저장된 값과 같은지 확인

 

perform() 메서드는 요청을 전송하는 역할을 하는 메서드

결과로 ResultActions 객체를 반환하므로 ResultActions 객체는 반환값을 검증하고 확인하는 andExpect() 메서드를 제공해준다.

 

accept() 메서드는 요청을 보낼 때 무슨 타입으로 응답을 받을지 결정하는 메서드

JSON, XML 등 다양한 타입이 있지만 여기에서는 JSON을 받는다고 명시

 

andExpect() 메서드는 응답을 검증

TestController에서 만든 API는 응답으로 OK를 반환하므로 이에 해당하는 메서드인 isOk를 사용해 응답 코드가 OK인지 확인

 

jsonPath는 JSON 응답값의 값을 가져오는 역할을 하는 메서드

0번째 배열에 들어 있는 객체의 id, name값을 가져오고, 저장된 값과 같은지 확인

 

조금 더 연습을 위해 main>java>패키지 폴더에서 다음과 같은 java 클래스를 만든다.

package me.shinsunyoung.springbottdeveloper;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
public class QuizController {

    @GetMapping("/quiz") // 1
    public ResponseEntity<String> quiz(@RequestParam("code") int code) {
        switch (code) {
            case 1:
                return ResponseEntity.created(null).body("Created!");
            case 2:
                return ResponseEntity.badRequest().body("Bad Request!");
            default:
                return ResponseEntity.ok().body("OK!");
        }
    }

    @PostMapping("/quiz") // 2
    public ResponseEntity<String> quiz2(@RequestBody Code code) {
        switch (code.value()) {
            case 1:
                return ResponseEntity.status(403).body("Forbidden!");
            default:
                return ResponseEntity.ok().body("OK!");
        }
    }
}
record Code(int value) {} // 3

quiz API로 get 요청이 오면 code에 따라 다른 요청을 처리한다.

반면 post 요청이 오면 forbidden이 요청된다.

 

그리고 test>java>패키지 폴더에는 다음과 같이 위의 코드를 테스트하기 위한 코드를 작성한다.

package me.shinsunyoung.springbottdeveloper;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.Mock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@SpringBootTest
@AutoConfigureMockMvc
public class QuizControllerTest {
    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
    }
    
}

 

ObjectMapper는 Jackson 라이브러리에서 제공하는 클래스로, 객체와 JSON 간의 변환을 처리한다.

 

Code code = new Code(13)

objectMapper.writeValueAsString(code)

 

이러면 {'value' : 13} 과 같이 JSON 형태의 문자열로 객체가 변환되는데, 직렬화라고 하기도 한다.

 

이상으로 테스트 코드를 마친다.

반응형