Study/개발일지

[백엔드TIL] TDD (Test Driven Develpment) 원리

아이바 2023. 10. 17. 10:37

TDD (Test Driven Develpment)

  • 테스트 주도 개발
    -> 성공 테스트뿐만 아니라 실패 테스트까지 작성해야한다
  • 반복 테스트를 이용한 소프트웨어 방법론
  • 작은 단위의 테스트 먼저 설계 및 구축 후 테스트를 통과할 수 있는 코드를 짜는 것
  • 애자일 개발 방식 중 하나
    -> 코드 설계시 원하는 단계적 목표에 대해 설정하여 진행하고자 하는 것에 대한 결정 방향의 갭을 줄이고자 함

테스트 코드 작성 목적

  • 코드의 안정성 높일 수 있음
  • 기능을 추가하거나 변경하는 과정에서 발생할 수 있는 side-effect를 줄일 수 있음
    -> A기능을 수정하기위해 어떤 메소드를 수정하는데, 이 메소드를 B기능에서도 사용하고있었을 때. B기능이 동작이 제대로 되지않을 수 있는데 이와 관련된 테스트코드 미리 작성해뒀다면 이를 이용해 side-effect방지가능
  • 해당 코드가 작성된 목적을 명확히 표현할 수 있음
    -> 코드에 불필요한 내용이 들어가는 것을 줄일 수 있음

필요 라이브러리

Java 단위테스트 작성에는 크게 2가지 라이브러리가 사용됨

  • Junit : 자바 단위 테스트를 위한 테스팅 프레임워크
  • AssertJ : 자바 테스트를 돕기 위해 다양한 문법을 지원하는 라이브러리

단위테스트

  • 코드의 특정 모듈이 의도된 대로 동작하는지 테스트하는 절차
  • 모든 함수와 메소드에 대한 각각의 테스트 케이스 작성하는 것

FIRST 원칙

  • Fast : 테스트는 빠르게 동작해 자주 돌릴 수 있어야한다.
    -> 테스트가 느리면 개발자가 테스트를 주저하게될 것
    -> 자주 검증하지 않은 소스코드는 버그가 발생활 확률 높아진다.
  • Independent : 독립적인 테스트가 가능해야함
    -> 테스트에 필요한 데이터는 테스트 내부에서 독립적으로 사용해야 함
    -> 데이터 존재 여부를 찾는 테스트가 있는 경우엔 해당 데이터는 테스트 내부에서 생성되어야 하며, 나중에 다른데 영향 미치지않게 제거해야함
  • Repeatable : 어느 환경에서도 반복가능해야하고, 매번 같은 결과를 만들어야 함
    -> 네트워크나 데이터베이스에 의존하지 않는 환경
    -> 환경에 의존하지 않는 테스트가 실패할 수 있는 이유는 오로지 테스트할 클래스 또는 메소드가 제대로 작동하지 않기때문
  • Self-Validating : 테스트는 그 자체로 실행하여 결과 확인 가능해야함
    -> 테스트가 실행될 때마다 메서드 출력이 올바른지 확인하는 것은 개발자가 결정해서는 안됨
  • Timely : 비즈니스 코드가 완성되기 전에 구성하고 테스트가 가능해야 함

Junit

  • 단위 테스트(Unit Test)를 위한 도구 제공
  • 어노테이션 기반 테스트 지원
  • 단정문(Assert)으로 테스트 케이스의 기대값에 대해 수행결과 확인 가능
  • Spring Boot 2.2버전부터 Junit 5버전 사용
  • Junit5는 크게 Jupiter,Platform,Vintage모듈로 구성됨

모듈 설명

JUnit Jupiter

  • TestEngine API구현체로 JUnit 5구현하고있음
  • 테스트의 실제 구현체는 별도 모듈 역할을 수행하는데, 그 모듈 중 하나가 Jupiter-Engine
  • 이 모듈은 Jupiter-API를 사용해 작성한 테스트 코드를 발견하고 실행하는 역할을 수행
  • 개발자가 테스트 코드 작성할 때 사용됨

JUnit Platform

  • Test 실행하기 위한 뼈대
  • Test를 발견하고 테스트 계획을 생성하는 TestEngine 인터페이스 갖고있음
  • TestEngine통해 Test발견하고 수행 및 결과 보고

JUnit Vintage

  • TestEngine API구현체로 JUnit 3,4를 구현하고 있음
  • 기존 JUnit 3,4 버전으로 테스트 코드 실행할 때 사용됨
  • Vintage-Engine모듈 포함하고있음

JUnit LifeCycle Annotation

JUnit 5기준

JUnit Main Annotaion

@SpringBootTest

  • 통합 테스트 용도
  • @SpringBootApplication을 찾아가 하위의 모든 Bean 스캔해 로드함
  • 그 후 Test용 Application Context를 만들어 Bean을 추가하고, 만약 MockBean으로 추가된게 있다면 해당 Bean을 MockBean으로 교체

@ExtendWith

  • JUnit4에서 @RunWith로 사용되던 어노테이션이 ExtendWith로 변경됨
  • @ExtendWith는 메인으로 실행될 Class를 지정할 수 있음
  • Mockito의 Mock 객체를 사용하기 위한 Annotation
  • @SpringBootTest는 기본적으로 @ExtendWith가 추가되어 있음

@WebMvcTest(Class명.class)

  • ()에 작성된 클래스만 실제로 로드하여 테스트 진행
  • 매개변수를 지정하지않으면 @Controller, @RestController, @RestControllerAdvice 등 컨트롤러와 연관된 Bean 모두 로드됨
  • 스프링의 모든 Bean을 로드하는 @SpringBootTest 대신 컨트롤러 관련 코드만 테스트할 경우 사용

'@Autowired' about Mockbean

  • Controller의 API를 테스트하는 용도인 MockMvc 객체를 주입받음
    -> MockMvc 객체 선언하고 그 위에 @Autowired 선언해야함
  • perform() 메소드를 활용해 컨트롤러의 동작 확인가능
    -> andExpect(), andDo(), andReturn() 등 메소드를 같이 활용함

@MockBean

  • 테스트할 클래스에서 주입 받고 있는 객체에 대해 가짜 객체를 생성해주는 어노테이션
  • 해당 객체는 실제 행위를 하지않음
  • given() 을 활용해 가짜 객체의 동작에 대해 정의하여 사용가능

@AutoConfigureMockMvc

  • spring.test.mockmvc의 설정을 로드하면서 MockMvc의 의존성 자동주입
  • MockMvc클래스는 REST API 테스트를 할 수 있는 클래스

@Import

  • 필요한 Class들을 Configuration으로 만들어 사용가능
  • Configuration Component클래스도 의존성 설정가능
  • Import된 클래스는 주입으로 사용 가능

Spring Boot 프로젝트의 테스트 코드

프로젝트를 열어보면 기본적으로 src- main, src-test 폴더구조가 생성되어있는데, main과 test의 구조가 동일한것을 볼 수 있다.
-> 테스트하기 위해서는 이후 생성하는 파일들도 main과 경로를 맞춰줘야한다!

test에 있는 '${프로젝트명}ApplicationTests'파일을 보면 기본적으로 아래와같이 코드가 생성되어있다.

@SpringBootTest
class SeatSenceApplicationTests {

	@Test
	void contextLoads() {
	}

}

@Test가 붙어있는 void contextLoads()는 뭘까?
-> @SpringBootTest 통합테스트 어노테이션이 붙어있기때문에, 모든 컨텍스트가 로드되어있는지 테스트해보는. 기본적으로 생성되어있는 테스트이다.

테스트 코드는 아니지만, 동작을 보기위해 기본적인 어노테이션들을 살펴보자.

기본동작

package test;

import org.junit.jupiter.api.*;

public class TestLifeCycle {

    @BeforeAll
    static void beforeAll() {
        System.out.println("## BeforeAll Annotation 호출 ##");
        System.out.println();
    }

    @AfterAll
    static void afterAll() {
        System.out.println("## BeforeAll Annotation 호출 ##");
        System.out.println();
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("## BeforeEach Annotation 호출 ##");
        System.out.println();
    }

    @AfterEach
    void afterEach() {
        System.out.println("## AfterEach Annotation 호출 ##");
        System.out.println();
    }

    @Test
    void test1() {
        System.out.println("## test1 시작 ##");
        System.out.println();
    }

    @Test
    @DisplayName("Test Case 2!!!")
    void test2() {
        System.out.println("## test2 시작 ##");
        System.out.println();
    }

    @Test
    @Disabled // Disabled Annotation : 테스트를 실행하지 않게 설정
    void test3() {
        System.out.println("## test3 시작 ##");
        System.out.println();
    }

}

위와같은 결과를 볼 수 있다.
커서가 눌린 두번째 줄은 클래스명이고, 그 아래는 Test되는 메소드의 이름이 뜬다.
-> DisplayName으로 이름을 설정해뒀으면 해당 이름이 뜬다.
test3의 옆에있는 표시는 테스트가 수행되지 않았음을 뜻한다. (Disabled 설정)


동작 순서는 위 이미지와 같다.

Spring Boot 프로젝트의 Controller 테스트 코드

ProductController.class파일을 테스트하기위한 'ProductControllerTest' 소스파일을 생성한다.

@MockBean으로 ProductServiceImpl은 왜 생성하는걸까?

ProductController 코드를 보면, ProductService를 Autowired해서 주입받고있다.

따라서 ProductServiceImpl에 대한 Mock 객체를 생성하는것이다.

이어서 테스트 코드를 계속보자.

given() : Mock객체가 특정 상황에서 해야하는 행위를 정의하는 메소드. 어떤 상황이 주어진다고 생각

-> mokito 라이브러리에서 가져온 메소드
-> mokito : mock 객체를 생성, 사용하는데 도와주는 라이브러리
-> MockBean으로 만든 Mock객체 (productService) 사용

  • getProduct() : productDto객체를 return해주는 메소드
    -> 따라서 코드에서보면 getProduct.willlReturn의 파라미터에 new ProductDto로 객체를 생성하고있음
  • willReturn() : 어떠한 값이 넘어올거야

andExpect() : 기대하는 값이 나왔는지 체크해볼 수 있는 메소드

-> Builder구조이기때문에, .을 구분해서 사용함

  • perform() : REST API 테스트할 수 있는 환경 만들어줌
  • get() : 실제로 우리가 어떤 통신을 할건지에대해 정의해줌. get이라는 Http Request 날릴 예정
    -> 파라미터 : 경로 (로컬이기때문에, /api 앞부분은 (포트번호까지) 제외가능
  • status().isOk() : status가 'ok'로 나왔는지 확인
  • jsonPath() : Http Request를 날리면 기본적으로 json형태의 바디값을 받기때문에, 파라미터에 있는 값들이
    -> exists() : 존재하는지 확인
  • andDo() : 요청에 대한 처리
    -> print() : 위에서 테스트한 내용을 출력

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

위 코드상에서는 productService라는 객체에서 "12315"로 getProduct가 실행되었는지 체크

그 다음 'createProductTest' 코드를 보자.

given()을 이용해서 saveProduct()가 호출된다면 가정해주고, willReturn()을 리턴해주는지 사전 세팅

post 통신을 할것이기때문에, dto객체를 먼저 만들어준다.
ProductDto.builder()..

  • Gson : Google에서 만든 Json의 형태를 자유롭게 다룰 수 있게 편의를 제공하는 라이브러리
    -> toJson(productDto) : productDto값을 Json으로 변경
  • post() : 파라미터에 경로지정
    -> .content() : 어떤 바디값을 넘겨줄건지
    -> .contentType() :

verify()
-> saveProduct() 파라미터에 들어가는 객체가 saveProduct로 실행됐는지 검증

Spring Boot 프로젝트의 Service 테스트 코드

  • 내가 어떤 객체를 가져올지 모르겠으면 @SpringBootTest() 사용해 {}안에 묶어서 실행
    -> 매개변수 없으면 전체 Bean로드
  • @ExtendWith, @Import 방식
    @ExtendWith는 @SpringBootTest에 포함되어있으므로, 필요한 내용만 갖다쓴 것
  • @MockBean으로 Handler생성은 뭘까?
    Service의 코드를 보자.

    Handler라는 객체를 주입받아 사용중이다.
    따라서 Test코드에서 Mock객체로 생성해서 사용해야한다.
  • @Autowired로 ServiceImpl?
    Controller테스트가 아니기때문에 (WebMvc 테스트가 아니기때문에), 테스트하고자 하는 객체를 주입받아 사용해야한다.
  • given : Mokito.when() 이런상황일 때, thenReturn() 이걸 리턴해줘라

(위에 잘린부분 이어서)

Service의 또 다른 테스트

728x90