티스토리 뷰

TDD(Test-Driven Development, 테스트 주도 개발)란?

실패-성공-리팩토링의 사이클을 반복하는 개발 방법.

실패 단계에선 지금 구현하고자 하는 기능에 실패하는 테스트 코드(케이스)를 작성한다.
성공 단계에선 실패하는 테스트 코드(케이스)가 통과하는 제대로 된 코드(기능)를 작성한다.
마지막으로 구현한 코드에서 리팩토링할 부분이 있는지 찾고, 있으면 리팩토링한 후에도 테스트 코드(케이스)가 통과하는 확인한다.
이 3단계가 끝나면 다음 기능을 대상으로 다시 사이클을 반복한다.

TDD는 기능별로 구현이 이루어지기 때문에 유지보수/디버깅이 쉽고, 기능별 모듈화가 잘 이루어진다는 장점이 있다!

그림 출처: https://sehun-kim.github.io/sehun/tdd/



단위 테스트(Unit Test)란?

기능(메소드)를 테스트하는 메소드

TDD의 실패 단계인 "기능 단위의 테스트 코드 작성"을 말함.
구현한 기능을 테스트 하기 위해 프로그램 전체를 실행시킬 필요를 없애준다.(테스트 코드로 확인 가능)
버그를 줄이고, 코드의 퀄리티를 높일 수 있다.


오늘 포스팅은 단위 테스트에 관련된 내용이다!
TDD? 단위 테스트? 어렴풋하게 알고는 있지만 제대로 숙지하고 있는 내용이 아니라 간단하게 정리하고 시작해봤다 ㅎㅎ

원래는 기능을 하나 구현하고나면 톰캣을 올려서 postman으로 요청해보고.. 틀렸으면 다시 코드 고치고 톰캣 올리고 postman.......
이렇게 코드를 수정할 때마다 수도없이 했던 반복을 테스트 코드로 대체할 수 있다고 하네요 대박..
위에 적은 내용보다 더 많은 장점들이 있다고하니 사용하면서 직접 느껴봅시다 ㅎㅎ

테스트 코드 작성을 돕는 프레임워크에는 JUnit, DBUnit, CppUnit, NUnit...등 XXUnit이 있습니다.
저는 여기서 JUnit4를 사용해보겠습니다!


테스트 코드 작성하기

지난번에 만든 Spring Boot 프로젝트에서 src/main/java 우클릭 -> New -> Package를 차례로 클릭해 패키지를 생성합니다.
패키지명은 주로 웹사이트 주소의 역순을 사용합니다.(ex. springboot.test.com -> com.test.springboot)

그 다음 패키지에서 package_name 우클릭 -> New -> Java Class를 차례로 클릭해 Java 클래스를 생성합니다.
클래스명은 Applicaion으로 지정합니다(main class).

Application.java의 내용을 다음과 같이 수정합니다.


package com.test.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args){
        SpringApplication.run(Application.class, args);
    }
}
  • @SpringBootApplication: Spring Boot 자동 설정, Spring Bean 읽기・생성 자동. 이 위치부터 설정을 읽기 때문에 항상 프로젝트의 최상단에 위치 해야함
  • SpringApplication.run(): 내장 WAS(Web Application Server) 실행
    • 내장 WAS란? 웹 어플리케이션과 서버 환경을 만들어 동작시키는 기능을 제공하는 프레임워크. 동적 콘텐츠 제공을 위한 어플리케이션 서버. 대표적으로 아파치 톰캣이 있음.
      Web Server(분산 트랜잭션, 스레드 처리 등 분산 환경, 정적 콘텐츠 제공) + Web Container(JSP, Servlet을 실행할 수 있는 SW)로 구성.
      어플리케이션 실행 시 내부에서 WAS가 실행됨.

위에서 생성한 패키지 아래에 web이라는 이름의 패키지를 생성합니다.(com.test.sprintboot.web으로 패키지명을 지정하면 됩니다.)
그 다음 web 패키지 아래에 HelloController라는 이름의 Java Class를 생성합니다.

이제 간단한 API를 만들겠습니다.
다음과 같이 HelloController.java를 수정합니다.


package com.test.springboot.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "hello";
    }
}

우선 HelloController 클래스를 RestController로 지정합니다.(@RestController, JSON을 반환하는 컨트롤러)
hello 문자열을 리턴하는 간단한 메소드를 작성하고, HTTP의 Get 요청을 받는 API로 선언해줍니다.(@GetMapping("/hello"), /hello 요청을 받으면 hello를 리턴)

이제 hello() 메소드를 테스트할 테스트 코드를 작성하겠습니다.
src/test/java/ 위치에 src/main/java/ 아래에 있는 것과 동일한 패키지를 생성합니다.

그다음 생성한 패키지 아래에 테스트 코드를 작성할 클래스(테스트 클래스)를 생성합니다.(HelloControllerTest)
아래의 코드와 같이 HelloControllerTest.java를 수정합니다.


package com.test.springboot;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

@RunWith(SpringRunner.class)
@WebMvcTest
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    public void testHello() throws Exception{
        String hello = "hello";

        mvc.perform(get("/hello"))
            .andExpect(status().isOk())
            .andExpect(content().string(hello));
    }
}

HelloControlTest 클래스에 RunWith 어노테이션을 선언합니다.(@RunWith(SpringRunner.class))
RunWith는 테스트 진행시, JUnit에 내장된 실행자가 아닌 사용자가 지정한 실행자(SpringRunner)를 실행하는 역할입니다.
SpringRunner는 테스트 클래스에 Spring ApplicationContext를 로딩하고, @Autowired로 Bean을 이용할 수 있게 해주는 실행자입니다.(테스트 클래스를 실행하기 위해 필요한 클래스)

그다음 WebMvcTest 어노테이션도 HelloControlTest 클래스에 선언합니다.(@WebMvcTest)
WebMvcTest를 선언하면, @Controllor 어노테이션을 사용할 수 있습니다.(@Service, @Component, Repository 사용X)

private으로 MockMvc타입의 mvc 변수를 선언합니다.(private MockMvc mvc)
MockMcv는 웹 API를 테스트할 때 사용하는 클래스입니다.(웹 어플리케이션을 웹 서버에 배포하지 않고도 Spring MVC 동작을 재현할 수 있게 해줌)
그리고 Autowired 어노테이션을 선언합니다.(@Autowired, 타입에 맞는 Bean을 해당 변수에 자동으로 주입)

HelloController에 작성한 hello() 메소드를 테스트하기 위한 testHello() 메소드를 작성합니다.
mvc.perform(get("/hello"))은 "/hello" 주소로 HTTP get 요청을 합니다.
andExpect(status.isOk())는 mvc.perform의 결과(200, 404, 500 등의 상태)를 확인합니다. isOk는 동작이 정상적으로 완료되었는지(200)를 확인합니다.
andExpect(content.string(hello))는 요청으로 인해 받은 값이 제대로 된 값인지 확인합니다.
그리고 Test 어노테이션으로 testHello() 메소드가 테스트 메소드임을 선언합니다.(@Test) 이 어노테이션이 붙은 메소드는 JUnit이 알아서 실행해줍니다.
get(), status(), content()에서 에러가 발생한다면, import 코드를 직접 작성해주면 됩니다!

테스트 코드 작성이 완료되었습니다!
하나하나 의미하는 것이 무엇인지 찾아보고 하느라 이 짧은 코드 작성에도 꽤 걸렸네요ㅠ.ㅠ
이제 작성한 테스트 코드를 실행해보겠습니다.

테스트 메소드가 작성된 줄의 왼쪽에 보이는 초록색 화살표를 클릭하면 다음과 같은 선택지가 보입니다.
여기서 첫번째에 보이는 Run 'testHello()를 클릭합니다.

status.isOk()와 content()를 무사히 통과한 결과를 확인할 수 있습니다! 오예ㅎ-ㅎ

존재하지 않는 주소값이나 요청으로 받은 값(content)를 다른 값으로 변경해보면?

위와 같이 테스트에 실패한 것을 확인할 수 있습니다.


마지막으로 실제로도 실행히 잘 되는지 확인해보겠습니다.
src/main/java/com.test.springboot/Application.java로 이동해 main() 메소드가 선언된 줄의 왼쪽에 있는 초록색 화살표를 클릭하고, Run 'Application.main()' 을 클릭해 서버를 올려줍니다.

로그에서 build.gradle에서 지정했던 Spring Boot 버전(2.7.1)으로 프로젝트가 실행되는 것과 톰캣 서버가 8080 포트로 실행되었다는 것을 확인할 수 있습니다.
이제 웹 브라우저에서 localhost:8080/hello로 접속해 결과가 잘 출력되는지 테스트해보겠습니다.

쨔잔! 요청이 정상적으로 이루어져 hello가 출력된 것을 확인할 수 있습니다.
이처럼 웹 개발시 구현하고자하는 다른 기능들도 이와같은 방법으로 테스트하고 구현하면 됩니다~!
메소드가 많아질수록 확실히 편할 것 같다는 생각이 드네요 ㅎㅎ
다른 기능을 구현한 후에 이전 기능이 잘 동작하는지도 이미 작성한 테스트 코드를 통해 쉽게 확인할 수 있고, 앞으로 꽤 많은 부분이 편해질 것 같네용ㅎ-ㅎ

고생하셨습니다!


*스프링부트와 AWS로 혼자 구현하는 웹 서비스(이동욱 지음) 책을 따라 공부하며 정리하는 포스트입니다.