본문 바로가기

개발(합니다)/방법론

Spring의 TDD 특징 및 사용법 정리

반응형

좋은 개발자가 되는 첫 걸음으로 TDD(Test Driven Development:테스트 주고 개발)을 정리합니다.

정리 후 의도적으로 실습하는 연습을 할 예정입니다.

 

 

 

 

테스트 주도 개발의  목적과 목표

-목적
  • 새로운 버그의 발생을 즉시 파악
  • 잘 잘동하는 깔끔한 코드
  • 방치 된 1개의 실패는 전체의 실패(100-1=0)
- 목표

1. 버그 발생을 파악 할 수 있어야 합니다.

2. 내일, 모레, 1년, 10년 후에도 정상 동작해야 합니다.

3. 재사용 가능해야 합니다.

4. 자동화 가능해야 합니다.

5. 수정/보완 된 코드로 인해 기존 코드에 버그가 발생하지 않음을 보장합니다.

테스트 주도 개발을 해야 하는 이유

 

버그잡이는 삽질이 아니고 버그잡이는 개발의 일부입니다.

버그 발생의 원인을 인지하는 간격이 멀면 삽질스러워집니다.

-> 버그 이유를 찾아야 한다. -> 분석한다 -> 수정한다. -> 반영한다. -> 테스트 한다. 

성공하면 다행이고 아니면 처음부터 다시 해야 하므로 삽질스러워집니다.

 

테스트 주도 개발 작성시 이점

1. 기능에 집중 할 수 있습니다.

코드 구현 -> 서버 실행 -> 수동 입력 -> 실행 -> 에러 -> 에러 분석(오래 걸림) -> 해결 or 버그(다시 처음부터)
테스트를 작성하면 에러를 즉시 확인 할 수 있어서 위에 반복 과정이 줄어듬

2. 작성 한 코드만 필요합니다.

DB 연동, 서버 연결 없이 테스트 할 수 있습니다.

 

테스트 주도 개발을 하기 위한 방법 정리

아직 구현하지도 않은 상태에서 테스트를 구현하는 사고 방식에 적응이 필요합니다.

처음에는 의식적인 노력이 필요하지만 어느 순간부터는 일상이 됩니다.

개발의 목표는 완벽함이 아니라 작은 절차부터 순서대로 하는 것입니다.

어떤 데티일을 고민하느라 멈추지 않아야 합니다. 소프트웨어는 언제든 변경되기 때문입니다.

 

1. 입력과 출력 결정

입력 : 비밀번호

출력 : 비밀번도 강도나 참/거짓

 

2. 함수 시그니처 선택

시그니처란 매개변수와 결과를 어떻게 할지 정하는 것입니다.

 

3. 기능상 하나의 작은 관점으로 판단

보통은 이 단계에서 코드를 작성하기 때문에 TDD와 다른 부분입니다.
모든 경우의 수를 생각하지 않고 최소한의 동작에 집중해야 합니다.

빈문자열이라면? 아이디와 비밀번호가 같다면? 빈 문자열만 생각해봅니다.

 

4. 테스트 구현

함수가 어떻게 동작하는 테스트 하는 단계입니다.

 

5. 코드 구현

최소한의 작동 코드 구현

 

 

단위 테스트의 의미

전통적인 단위 테스트 : 제픔의 기능을 테스트

TDD에서 말하는 테스트 : 메소드 단위의 테스트

 

테스트 케이스를 작성하기 위한 팁

 

1. 요구 사항 이름의 테스트 케이스로 메소드 이름을 한글로 작성합니다.

2. 깨진 테스트 케이스는 삭제하고 커밋 전 리뷰로 보완합니다.

3. 시스템 테스트, 통합 테스트도 자동화 해야 합니다.

 

 

 

 

 

Spring MVC TEST를 사용 하는 이유

Spring mvc에서는 mock으로만 테스트 하기에는 한계가 있습니다.

@RequestMapping, View Resolving을 테스트 할 수 없습니다.

 

Spring mvc를 MockMVC로 묶어 Request, Response를 테스트 할 수 있습니다.

 

 

간단 테스트 케이스 작성

pom.xml
        <!-- Servlet -->
        <!-- <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>servlet-api</artifactId>
            <version>2.5</version>
            <scope>provided</scope>
        </dependency> -->
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>

        <!-- 2018.12.21 otrodevym -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${org.springframework-version}</version>
        </dependency>
        
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>
시작하기 앞서 test 라이브러리를 추가하고 버전을 올려줘야합니다.
 
 
 
HomeContoller.java
package com.otrodevym.test;

import javax.inject.Inject;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = "file:src/main/webapp/WEB-INF/spring/**/*.xml")
public class SampleControllerTest {

    private static final Logger loger = LoggerFactory.getLogger(SampleControllerTest.class);

    @Inject
    private WebApplicationContext wac;
    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
    }

    @Test
    public void Test() throws Exception {
        mockMvc.perform(MockMvcRequestBuilders.get("/"))
        .andExpect(MockMvcResultMatchers.view().name("home"));
    }

}

homeController를 테스트 작성

 

StandAloneTest.java

package com.otrodevym.test;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

public class StandAloneTest {

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new HomeController()).build();
    }

    @Test
    public void test() throws Exception {
        this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
                .andExpect(model().attributeExists("serverTime")).andReturn();
    }

}

설정 없이 작동하는 테스트 케이스(단독 모드)

 

StandAloneTest.java

package com.otrodevym.test;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations
= "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml")
public class StandAloneTest {

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.standaloneSetup(new HomeController()).build();
    }

    @Test
    public void test() throws Exception {
        this.mockMvc.perform(get("/")).andDo(print()).andExpect(status().isOk())
                .andExpect(model().attributeExists("serverTime")).andReturn();
    }

}

설정을 참조하는 테스트 케이스(사용자 정의 DI 컨테이너 모드)

 

스프링 테스트 설명

-어노테이션
 
@RunWith

JUnit 확장 하는 어노테이션이며 SpringJUit4classRunner로 JUnit용 테스트 컨텍스트 프레임 워크 확장 클래스를 지정하면 테스트 진행 시 컨텍스트를 생성하고 관리하는 작업을 합니다.

지정하지 않으면 SpringRunner.class로 사용됩니다.

 

@ContextConfiguration

Spring Bean 메타 설정 파일의 위치를 지정합니다.

지정하지 않는다면 테스트 파일의 패키지에서 설정 파일을 찾습니다.

ContextConfigLocationTest-context.xml or contextconfigLocationtest-context.xml 대소 구분 없음

 

@WebAppConfiguration

웹 어플리케이션 전용 DI 컨테이너로 처리합니다.

 

@ignore

해당 테스트를 테스트를 하지 않습니다.

 

@Test

단위 테스트를 선언합니다.

@Test(timeout=6000) 단위는 밀리, 해당 시간을 넘기면 실패합니다.

@Test(expected=NullPointerException)은 NullPointerException이 발생해야 통과입니다.

 

@Before

테스트 이전에 실행 할 메소드를 지정합니다.

테스트 메소드가 실행 될 때마다 객체를 생성하여 실행합니다.

 

@After

테스트 이후에 실행 할 메소드를 지정합니다.

테스트 메소드가 실행 될 때마다 객체를 생성하여 실행합니다.

 

@BeforeClass

테스트 이전에 실행 할 메소드를 지정합니다.

@Before과 차이점은 한번만 실행되며 static으로 선언해야 합니다.

 

@Afterclass

테스트 이후에 실행 할 메소드를 지정합니다.

@Afterd와 차이점은 한번만 실행되며 static으로 선언해야 합니다.

 

 

 

 

 

-클래스/메소드

 

MockMvc
서버를 실행 하지 않고 스프링 MVC 동작을 재현할 수 있는 클래스
Mockmvc는 TestDispatcherServlet에게 요청
 
perform()
DispatherServlet에 요청
get, post, delete, put, fileUpload 등 메소드 제공
ResultActions() 호출
 
MockHttpServletRequestBuilder()
param/ params : 요청 파라미터 설정
header/ headers : 요청 헤더 설정
cookie : 쿠키 설정
content : 요청 본문 설정
requestAttr : 요청 스코프에 객체 설정
flashAttr : 플래시 스코프에 객체를 설정
sessionAttr : 세션 스코프에 객체를 설정
 
MockMvcResultMatchers()
status : HTTP 상태 코드 검증
header : 응답 헤더의 상태 검증
cookie : 쿠키 상태 검증
content : 응답 한 본문 내용 검증
view : 반환 된 뷰 이름 검증
forwardedUrl : 경로 검증
redirectedUrl : 경로나 url 검증
model : 모델 상태 검증
flash : 플래시 스코프 상태 검증
request : 비동기 처리의 상태나 요청 스코프의 상태, 세션 스코프 상태 검증
 
ResultActions().andExpect()
실행 결과를 검증 인수 설정
 
ResultActions().andDo()
실행 결과를 처리 할 수 있는 인수 지정

 

log()

디버깅 레벨에서 로그를 출렵합니다.

 

print()

실행 결과를 출력합니다.

 

assert

assertArrayEquals(a,b) : 배열 a와  b가 같은지 확인합니다.

assertEquals(a,b) : a와 b가 같은지 확인합니다.

assertSame(a,b) : a와 b가 같은 객체인지 확인합니다.

assertTrie(a) : a가 True인지 확인 합니다.

assertNotNull(a) : a가 null이 아닌지 확인합니다.

 

 

 

 

 

반응형