스프링 부트 프로젝트를 기반으로 테스트 주도 개발을 적용하여 단계별 구축을 진행해 본다.

테스트 주도 개발(Test Driven Development)

실제 구현될 코드를 작성하기 전에 테스트 코드를 먼저 작성해가면서 개발을 진행하는 방식이다.

유명한 TDD 방법론이 널리 알려져 사용되고 있다.

TDD 방법론 자체가 광범위 하기 때문에 모든 내용을 다 담을 수 없으므로 본 글에서 스프링 부트 생성 부터 간단한 REST API를 구축까지만 진행해 보도록 하자.

여기서는 간단히 두가지 규칙만을 적용하도록 한다.

  • 반드시 구현 클래스를 작성하기 전에 테스트 클래스를 먼저 작성한다.
  • 1개의 구현 클래스당 1개 이상의 테스트 클래스를 꼭 작성한다.

아래 과정을 진행하기 전에 스프링 부트 프로젝트 생성에 대해서는 이전 글 스프링 부트 (Spring boot) 소개 에서 확인할 수 있다.

I. 시작 프로젝트 만들기

1. 프로젝트 생성

  • Spring Initializr 에 접속해서 프로젝트를 생성하거나 통합개발환경에서 스프링 부트 프로젝트를 시작한다.
  • 프로젝트 생성시 선택하는 Dependencies 는 “Web”, “Lombok”, “JPA”, “H2” 를 필수로 선택한다.
  • 여기서 프로젝트명은 “sbtdd01”, 패캐지명은 “com.joongang” 입력한 기준으로 설명하도록 한다.

스프링 부트 프로젝트 생성

2. 테스트 폴더 확인

자동 생성된 스프링 부트 프로젝트에는 기본적으로 “/src/test/java/com.joongang.sbtdd01” 폴더가 존재한다.

이 폴더 아래에 클래스 파일을 생성하면, 테스트 모드로 실행시에 이 파일들이 실행이 된다.

스프링 부트 프로젝트 구성

II. 도메인 테스트 하기

1. 테스트 파일 생성

“/src/test/java/com.joongang.sbtdd01” 폴더 아래에 “T01DomainTest.java” 파일을 생성한다.
(파일명에 “T01” 접두어를 추가한 이유는 파일을 이름순으로 정렬하고 테스트순서를 지정하기 위해서이다.)

이 파일에 아래 내용을 작성해 넣는다.

package com.joongang.sbtdd01;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class T01DomainTest {

    @Test
    public void t01_when_createdomain_except_notnull() {
    }
}

2. 테스트 파일 실행

대부분의 통합개발환경에서 “T01DomainTest.java” 를 선택하고 “Run …” 을 실행하면 테스트 파일이 실행이 된다.
(IntelliJ 기준으로 파일 선택 후 마우스 오른쪽 버튼 클릭 -> “Run T01DomainTest.java” 선택)

테스트 파일이 실행이 되면 아래와 같이 테스트 결과가 표시가 된다.

현재는 테스트 코드가 작성된것이 없으므로 녹색 아이콘으로 정상적으로 테스트를 패스한것으로 표시가 된다.

테스트 실행

3. 테스트 코드 추가

t01_when_createdomain_except_notnull() 함수 안에 아래 코드를 추가한다.

    @Test
    public void t01_when_createdomain_except_notnull() {
        //act
        Product product = new Product(1L, "iPad mini 64GB", 500.20, 5);
        
        //assert
        Assertions.assertThat(product).isNotNull();
        Assertions.assertThat(product.getId()).isEqualTo(1L);
        Assertions.assertThat(product.getTitle()).isEqualTo("iPad mini 64GB");
    }

이 테스트 코드에서는 Product 클래스의 객체 인스턴스를 생성하고 아래의 테스트를 진행하도록 테스트 코드를 작성하였다.

  • 이 객체의 인스턴스가 null이 아닌것이 맞는지 테스트
  • ID값이 1이 맞는지 테스트
  • Title이 “iPad mini 64GB”가 맞는지 테스트

그런데 Product 클래스를 만들지 않고 테스트 코드 먼저 작성했기 때문에, 통합개발환경에서는 여러 라인에서 에러가 나올것이다.

4. 도메인 클래스 생성

“/src/main/java/com.joongang.sbtdd01” 폴더 아래에 “Product.java” 파일을 생성한다.

이 파일에 아래 내용을 작성해 넣는다.

package com.joongang.sbtdd01;

import lombok.AllArgsConstructor;
import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
    @Id
    private Long id;
    
    private String title;
    
    private Double price;
    
    private Integer inventory;
}

다시 테스트 파일 “T01DomainTest.java”를 열어보면 Product 클래스가 구현이 되었기 때문에 에러가 사라져 있을것이다.

5. 테스트 파일 실행

테스트 파일을 다시 실행해 보면 녹색 아이콘으로 정상적으로 테스트를 패스한것으로 표시가 된다.

테스트 실행

III. 레파지토리 테스트 하기

이번에는 JPA로 데이타를 읽고 쓰는 레파지토리를 테스트 해보도록 한다.

1. 테스트 파일 생성

“/src/test/java/com.joongang.sbtdd01” 폴더 아래에 “T02RepositoryTest.java” 파일을 생성한다.

이 파일에 아래 내용을 작성해 넣는다.

package com.joongang.sbtdd01;

import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class T02RepositoryTest {

    @Autowired
    ProductRepository productRepository;

    @Test
    public void t01_when_savedomain_except_notnull() {
        //act
        Product product = new Product(1L, "iPad mini 64GB", 500.20, 5);

        Product persistProduct = productRepository.save(product);

        //assert
        Assertions.assertThat(persistProduct).isNotNull();
        Assertions.assertThat(persistProduct.getId()).isEqualTo(1L);
        Assertions.assertThat(persistProduct.getTitle()).isEqualTo("iPad mini 64GB");
    }
}

이 테스트 코드에서는
ProductRepository 클래스의 객체 인스턴스를 @Autowired로 주입받아 생성해 두고
Product 클래스의 객체 인스턴스를 생성한 뒤
ProductRepository를 통해 저장 한 뒤 다시 받아서
아래의 테스트를 진행하도록 테스트 코드를 작성하였다.

  • 이 인스턴스가 null이 아닌것이 맞는지 테스트
  • ID값이 1이 맞는지 테스트
  • Title이 “iPad mini 64GB”가 맞는지 테스트

마찬가지로 ProductRepository 클래스를 만들지 않고 테스트 코드 먼저 작성했기 때문에, 통합개발환경에서는 여러 라인에서 에러가 나올것이다.

2. 레파지토리 클래스 생성

“/src/main/java/com.joongang.sbtdd01” 폴더 아래에 “ProductRepository.java” 파일을 생성한다.

이 파일에 아래 내용을 작성해 넣는다.

package com.joongang.sbtdd01;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}

다시 테스트 파일 “T02RepositoryTest.java”를 열어보면 ProductRepository 인터페이스 구현이 되었기 때문에 에러가 사라져 있을것이다.

3. 테스트 파일 실행

테스트 파일을 다시 실행해 보면 녹색 아이콘으로 정상적으로 테스트를 패스한것으로 표시가 된다.

테스트 실행

IV. 컨트롤러 테스트 하기

이번에는 JPA로 데이타를 읽고 쓰는 레파지토리를 테스트 해보도록 한다.

1. 테스트 파일 생성

“/src/test/java/com.joongang.sbtdd01” 폴더 아래에 “T03ControllerTest.java” 파일을 생성한다.

이 파일에 아래 내용을 작성해 넣는다.

package com.joongang.sbtdd01;

import org.assertj.core.api.Assertions;
import org.assertj.core.util.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;

import static org.mockito.BDDMockito.given;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class T03ControllerTest {

    @MockBean
    private ProductController productController;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void t01_when_getallproducts_except_productlist() {
        //arrange
        given(productController.getAllProducts()).willReturn(
                Lists.newArrayList(
                        new Product(1L, "iPad mini 64GB", 500.20, 5),
                        new Product(2L, "iPad Pro 128GB", 500.20, 5),
                        new Product(3L, "MacBook Pro Retina 256GB", 500.20, 5)
                ));

        //act
        ResponseEntity<Product[]> response = restTemplate
                .getForEntity("/products", Product[].class);

        //assert
        Assertions.assertThat(response).isNotNull();
        Assertions.assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        Product[] products = response.getBody();
        Assertions.assertThat(products).isNotNull();
        Assertions.assertThat(products.length).isGreaterThan(0);
        Assertions.assertThat(products[0].getId()).isEqualTo(1L);
        Assertions.assertThat(products[0].getTitle()).isEqualTo("iPad mini 64GB");
    }
}

이 테스트 코드에서는
TestRestTemplate 클래스의 객체 인스턴스를 @Autowired로 주입받아 생성해 두고
ProductController 클래스의 MockBean을 주입받아 생성해 둔다.
@MockBean 으로 생성된 객체는 given() 함수에서 조건을 지정하고 이 조건에 해당하는 요청을 받았을때 실제 코드가 동작하는대신 테스트에서 임의로 리턴값을 변경할 수 있다.

아래의 각 테스트를 진행하도록 테스트 코드를 작성하였다.

  • REST 응답값이 Null 이 아닌지 테스트
  • REST 응답의 상태코드가 OK(200)인지 테스트
  • REST 응답의 바디가 Null 이 아닌지 테스트
  • REST 응답의 바디 결과 갯수가 0보다 큰지 테스트
  • REST 응답의 바디 결과의 첫번째 아이템의 Id가 1이 맞는지 테스트
  • REST 응답의 바디 결과의 첫번째 아이템의 Title이 “iPad mini 64GB”가 맞는지 테스트

마찬가지로 ProductController 클래스를 만들지 않고 테스트 코드 먼저 작성했기 때문에, 통합개발환경에서는 여러 라인에서 에러가 나올것이다.

2. 컨트롤러 클래스 생성

“/src/main/java/com.joongang.sbtdd01” 폴더 아래에 “ProductController.java” 파일을 생성한다.

이 파일에 아래 내용을 작성해 넣는다.

package com.joongang.sbtdd01;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import javax.persistence.EntityNotFoundException;
import java.util.List;

@RestController
public class ProductController {

    @Autowired
    ProductRepository productRepository;

    @GetMapping
    public List<Product> getAllProducts() {
        return productRepository.findAll();
    }

    @GetMapping("/products/{id}")
    public Product getAllProducts(@PathVariable("id") Long id) {
        return productRepository.findById(id).orElseThrow(EntityNotFoundException::new);
    }
}

다시 테스트 파일 “T03ControllerTest.java”를 열어보면 ProductController 클래스가 구현이 되었기 때문에 에러가 사라져 있을것이다.

3. 테스트 파일 실행

테스트 파일을 다시 실행해 보면 녹색 아이콘으로 정상적으로 테스트를 패스한것으로 표시가 된다.

테스트 실행


본 예제 결과물 sbtdd01.zip 소스파일은 아래 링크에서 다운받을 수 있다.
sbtdd01.zip 소스파일


Spring SpringBoot Back-end JPA Lombok H2 REST TDD

Blog Logo

HeeHun Kang


Published