우당탕탕 개발일지

[Spring] TDD(Test-Driven-Development), 테스트 주도 개발 정리하기 본문

Spring

[Spring] TDD(Test-Driven-Development), 테스트 주도 개발 정리하기

kyunge_ev 2022. 11. 18. 23:06

🌈 TDD 란?

  • Test-Driven-Development 의 약자로 테스트 주도 개발이라는 의미를 가진다.
  • 테스트를 먼저 설계 및 구축하고 테스트를 통과할 수 있는 코드를 짜는 것을 말한다.
  • 애자일(Agail) 개발 방식 중 하나
    • 코드 설계 시 원하는 단계적 목표에 대해 설정하여 진행하고자 하는 것에 대한 결정 방향의 갭을 줄임
    • 최초 목표에 맞춘 테스트를 구축하여 그에 맞게 코드를 설계하기 때문에 보다 적은 의견 충돌 기대

테스트코드의 목적

  • 개발 과정에서 문제를 미리 발견할 수 있음
  • 리팩토링의 리스크가 줄어듦
  • 애플리케이션을 가동해서 직접 테스트하는 것보다 테스트를 빠르게 진행할 수 있음
  • 하나의 명세 문서로서의 기능을 수행
  • 몇 가지 프레임워크에 맞춰 테스트 코드를 작성하면 좋은 코드를 생산할 수 있음
  • 코드가 작성된 목적을 명확하게 표현 할 수 있으며, 불필요한 내용이 추가되는 것을 방지

단위테스트 & 통합 테스트

📌 단위 테스트(Unit Tests)

  • 애플리케이션의 개별 모듈을 독립적으로 테스트하는 방식
  • 테스트 대상의 범위를 기준으로 가장 작은 단위의 테스트 방식
  • 테스트 비용이 적게 들어 테스트 피드백을 빠르게 받을 수 있음
  • 특정 모듈에 대한 테스트만 이뤄지기 때문에 데이터베이스나 네트워크 같은 외부 요인들은 제외하고 진행

📌 통합 테스트(Integration Tests)

  • 모듈을 통합하는 과정에서 호환성 등을 포함해 애플리케이션이 정상적으로 동작하는지 확인하기 위해 수행
  • 여러 모듈을 함께 테스트해서 정상적인 로직 수행이 가능한지 확인
  • 외부요인을 포함하여 테스트를 진행하므로 애플리케이션이 온전히 동작하는 지 확인 가능
  • 수행 할때마다 모든 컴포턴트가 동작해야 하기 때문에 비용이 커짐

테스트코드 작성방법

📌 Given-When-Then 패턴 

테스트 주도 개발에서 파생 된
BDD(Behavior-Driven-Development : 행위 주도 개발)를 통해 탄생한 테스트 접근 방식.

불필요한 코드가 늘어나기 때문에 단위 테스트에서는 잘 사용 하지 않고, '명세'문서의 역할을 주로 수행한다.

  • Given : 테스트 수행 전 테스트에 필요한 환경을 설정하는 단계(변수 정의, Mock 객체를 통해 특정상황의 행동 정의)
  • When : 테스트의 목적을 보여주는 단계. 실제 테스트 코드 포함, 테스트를 통한 결괏값을 가져옴
  • Then : 테스트의 결과를 검증하는 단계

📌 F.I.R.S.T 패턴

테스트코드를 작성하는데 도움이 될 수 있는 5가지 규칙을 의미. 주로 단위테스트에 적용할 수 있는 규칙.

  • 빠르게(Fast)
    : 테스트는 빠르게 수행되어야한다. 목적을 단순화, 외부 환경 요인을 사용하지 않으므로서 빠른 테스트를 수행
  • 고립된, 독립적(Isolated)
    : 하나의 테스트 코드는 목적으로 여기는 하나의 대상에 대해서만 수행되어야한다. 만약 다른 테스트코드와 상호작용하거나 관리할 수 없는 외부 소스를 사용하게 되면 외부 요인으로 인해 테스트가 수행되지 않을 수 있음
  • 반복 가능한(Repeatable) 
    : 테스트는 어떤 환경에서도 반복 가능해야한다. 개발 환경의 변화나 네트워크 연결 여부와 상관없이 수행되어야 한다.
  • 자가 검증(Self-Validating)
    : 테스트는 그 자체만으로도 테스트의 검증이 완료돼야함. 성공, 실패 여부를 확인 할 수 있는 코드를 함께 작성해야함.
  • 적시에(Timely)
    : 테스트 코드는 테스트하려는 애플리케이션 코드를 구현하기 전에 완성돼야함

🌈 JUnit을 활용한 테스트 코드 작성

JUnit 이란?

  • Java 언어에서 사용되는 대표적인 테스트 프레임워크로서 단위 테스트를 위한 도구들을 제공한다.
    (통합 테스트를 위한 도구 또한 제공함)
  • 어노테이션(@) 기반의 테스트 방식을 지원
  • 단정문(Assert)을 통해 테스트 케이스의 기댓값이 정상적으로 도출됐는지 검토할 수 있음
  • Jupiter, Platform, Vintage 3가지 모듈로 구성됨
    • 하나의 Platform 모듈을 기반으로 Jupiter와 Vintage 모듈이 구현체의 역할을 수행함
  • SpringBoot는 테스트 환경을 쉽게 설정할 수 있게 'spring-boot-starter-test' 프로젝트를 지원
    • gradle.build 에 의존성을 추가해야 사용가능

📌 JUnit Platform

  • JVM에서 테스트를 시작하기 위한 뼈대 역할
  • 테스트를 발견하고 계획을 생성하는 테스트 엔진(TestEngine)의 인터페이스를 가지고 있음
  • 각종 IDE와의 연동을 보조하는 역할을 수행(IDE 콘솔 출력 등)
  • TestEngine API, Console Launcher, JUnit 4 Based Runner 등 포함되어있음

📌 Junit Jupiter

  • 테스트 엔진 API의 구현체를 포함하고 있음
  • JUnit 5에서 제공하는 Jupiter 기반의 테스트를 실행하기 위한 테스트 엔진을 가지고 있음
  • Jupiter Engine은 Jupiter API를 활용해서 작성한 테스트 코드를 발견하고 실행하는 역할을 수행함

📌 Junit  Vintage

  • JUnit 3,4에 대한 테스트 엔진 API를 구현
  • 기존에 작성된 JUnit 3,4 버전의 테스트 코드를 실행할 때 사용됨
  • Vintage Engine을 포함하고 있음

Junit 생명 주기

@Test : 테스트 코드를 포함한 메서드를 정의

@BeforeAll : 테스트를 시작하기 전에 호출되는 메서드를 정의

@BeforeEach : 각 테스트 메서드가 실행되기 전에 동작하는 메서드를 정의

@AfterAll : 테스트를 종료하면서 호출되는 메서드를 정의

@AfterEach : 각 테스트 메서드가 종료되면서 호출되는 메서드를 정의

@Disabled : 지정된 테스트는 실행하지 않음 단, 테스트 메서드로 인식되기 때문에 로그가 출력됨

🌈 컨트롤러, 서비스, 리포지토리 객체의 테스트

테스트 어노테이션 & 용어 정리

@WebMvcTest(사용할 컨트롤러명.class)
: 웹에서 사용되는 요청과 응답에 대한 테스트를 수행 한다. 만약, 대상 클래스를 추가하지 않으면 @Controller, @RestController, ControllerAdvice 등의 컨트롤러 관련 빈 객체가 모두 로드됨.

 

@AutoWired 
MockMvc mockMvc;
: 컨트롤러의 API를 테스트하기 위해 사용 된 객체
서블릿 컨테이너의 구동 없이 가상의 MVC 환경에서 모의 HTTP 서블릿을 요청하는 유틸리티 클래스

 

@MockBean
ArticleService articleService;
: MockBean을 등록하여 articleService 객체에 Mock 객체를 주입
, articleService 객체는 Controller가 의존성을 가지고 있던 객체

 

@DisplayName
: 테스트 메서드의 이름이 복잡해서 가독성이 떨어질 경우 해당 어노테이션으로 테스트에 대한 표현을 할 수 있음

 

@AutoWiredObjectMapper
: java object를 JSON 형식으로 바꿔주는 jackson 라이브러리

objectMapper.writeValueAsBytes([java object])
매개변수로 받는 java objet를 JSON 형식으로 바꿔줌

 

given() (Mockito에서 제공)
: Mock 객체가 특정 상황에서 해야하는 행위를 정의하는 메서드
해당 객체에서 어떤 메서드가 호출되고, 어떤 파라미터를 주입 받는지를 가정함
(아래의 코드에서는 Mock 객체인 AtricleService의 .findSigle 메서드를 호출하는 행위를 정의, 파라미터는 (1L))

 

willReturn()
: 어떤 결과를 리턴할 것인지 정의
(아래의 코드에서는 articleResponse 가 return 됨을 정의)

 

mockMvc.perform()
: 서버로 URL 요청을 보내는 것처럼 통신 테스트 코드를 작성해서 컨트롤러를 테스할 수 있음

mockMvc.perform(get("URL 경로")

 

.andExpect()
: 기대하는 값이 나왔는지 체크하는 메서드, builder 구조로 되어있어서 '.' 으로 구분

 

jsonPath("$.id")
: $(root)를 표시 하위 element에 접근하려면 .Element_Name의 형식 사용
json의 key 값을 찾을 수 있는 문법

 

status().isOk()
: 상태 코드 200 으로 정상

 

.exists()
: 해당하는 key값이 존재하는지 확인

 

.andDo(print())
: 값을 출력

 

verify()
: 해당 객체의 메서드가 실행되었는지 체크

 

👀 실습 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import com.fasterxml.jackson.databind.ObjectMapper;
import com.mustache.bbs1.domain.dto.ArticleAddRequest;
import com.mustache.bbs1.domain.dto.ArticleAddResponse;
import com.mustache.bbs1.domain.dto.ArticleResponse;
import com.mustache.bbs1.service.ArticleService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.mockito.BDDMockito.given;
 
@WebMvcTest(ArticleRestController.class)
class ArticleRestControllerTest {
 
    @Autowired
    MockMvc mockMvc;
 
    @MockBean
    ArticleService articleService;
 
    @Autowired
    ObjectMapper objectMapper;
 
    @Test
    @DisplayName("해당 id의 글이 조회가 잘 되는지")
    void findSingle() throws Exception {
        ArticleResponse articleResponse = ArticleResponse.builder()
                .id(1L)
                .title("안녕")
                .content("안녕")
                .build();
 
        given(articleService.getArticleById(1L)).willReturn(articleResponse);
        mockMvc.perform(get("/api/v1/articles/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").exists())
                .andExpect(jsonPath("$.title").exists())
                .andExpect(jsonPath("$.content").exists())
                .andDo(print());
 
        verify(articleService).getArticleById(1L);
    }
 
}
cs