테스트 코드 작성

짱J·2022년 5월 8일
0
post-thumbnail

테스트 코드 작성

TDD - 테스트가 주도하는 개발

  • 항상 실패하는 테스트를 먼저 작성하고
  • 테스트가 통과하는 프로덕션 코드를 작성하고
  • 테스트가 통과하면 프로덕션 코드를 리팩토링한다

단위 테스트는 TDD의 첫 번째 단계인 기능 단위의 테스트 코드를 작성하는 것을 이야기한다.
TDD와 달리 테스트 코드를 꼭 먼저 작성해야 하느 것도 아니고, 리팩토링도 포함되지 않는다.
순수하게 테스트 코드만 작성하는 것을 이야기한다.


테스트 코드는 왜 적어야 할까?

  • 단위 테스트는 개발 단계 초기에 문제를 발견할 수 있도록 도와준다.
  • 단위 테스트는 개발자가 나중에 코드를 리팩토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있다.
  • 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있다.
  • 단위 테스트는 시스템에 대한 실제 문서를 제공한다. 즉, 단위 테스트 자체가 문서로 사용될 수 있다.

테스트 코드가 없다면 아래와 같은 방식으로 개발이 진행된다.
(책의 필자의 경험담이며, 나 또한 이랬다.)

  1. 코드를 작성하고
  2. 프로그램을 실행한 뒤
  3. API 테스트 도구로 HTTP 요청하고
  4. 요청 결과를 System.out.println()으로 눈으로 검증하고
  5. 결과가 다르면 다시 프로그램을 중지하고 코드를 수정한다.

테스트 코드가 없다 보니 눈과 손으로 직접 수정된 기능을 확인할 수 밖에 없다.
그러나 테스트 코드를 작성하면 코드 수정이 발생할 대마다 위 과정이 반복되는 문제가 해결된다.
두 번째로, 테스트 코드를 작성하면 자동검증이 가능하다.
세 번째로, 개발자가 만든 기능을 안전하게 보호해준다.
테스트 코드는 새로운 기능이 추가될 때, 기존 기능이 잘 작동되는 것을 보장해준다.

테스트 코드 작성을 도와주는 프레임워크는 대표적으로 xUnit 프레임워크가 있다.

  • JUnit - Java
  • DBUnit - DB
  • CppUnit - C++
  • NUnit - .net

이 중에서 우리는 자바용인 JUnit을 사용한다.

JUnit은 최근 버전 5까지 나왔지만 아직 대부분 4 버전을 사용한다.
(+ 근데 다른 블로그 글을 읽으면 Spring boot 2.2.X 부터는 5 버전이 default라고 한다. 그래도 JUnit4가 일반적인 것 같으니, 4 버전을 4용하자~)


Hello Controller 테스트 코드 작성하기

package com.example.demo;

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

// 스프링 부트의 자동 설정, 스프링 빈 읽기와 생성을 자동으로 설정
// 항상 프로젝트 최상단에 위치하여야 함
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
    	// SpringApplication.run으로 내장 WAS 실행
        SpringApplication.run(DemoApplication.class, args);
    }

}

DemoApplication 클래스의 코드이다.
DemoApplication 클래스는 앞으로 만들 프로젝트의 메인 클래스가 된다.

여기서 내장 WAS란, 별도로 외부에 WAS(Web Application Server)를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것을 이야기한다.
이렇게 되면 항상 서버에 톰캣을 설치할 필요가 없게 되고, 스프링 부트로 만들어진 JAR 파일로 실행하면 된다.

스프링 부트에서는 언제 어디서나 같은 환경에서 배포할 수 있도록 내장 WAS를 사용하는 것을 권장한다.
외장 WAS를 사용하게 되면 모든 서버는 WAS의 종류와 버전, 설정을 일치시켜야 한다.

이제 테스트를 위한 Controller를 만들어보자.


먼저 다음과 같이 web이라는 패키지를 만들었다.
앞으로 컨트롤러와 관련된 클래스들은 모두 이 패키지에 담는다.

테스트 코드 작성에 앞서 테스트를 진행할 간단한 API를 만든다.

package com.example.demo.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";
    }
}

🐝 @RestController

  • 컨트롤러를 JSON을 반환하는 컨트롤러로 만들어준다.
  • 예전에는 @ResponseBody를 각 메소드마다 선언했던 것을 한 번에 사용할 수 있게 만들어준다고 생각하면 된다.

🐝 @GetMapping

  • HTTP Method인 Get의 요청을 받을 수 있는 API를 만들어 준다.
  • /hello로 요청이 오면 문자열 hello를 반환하는 기능을 갖는다.

작성한 코드가 제대로 작동하는지 테스트 코드로 검증해 보자.

우선, src/test/java 디렉토리에 앞에서 생성했던 패키지를 그대로 다시 생성한다.
그리고 테스트 코드를 작성할 클래스인 테스트 클래스를 생성한다.
일반적으로 테스트 클래스는 대상 클래스 이름에 Test를 붙인다.

그리고 테스트 코드를 작성해준다.

package com.example.demo.web;

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 org.springframework.test.web.servlet.ResultActions;
import static  org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {
    
    @Autowired
    private MockMvc mvc;
    
    @Test
    public void hello가_리턴된다() throws Exception {
        String hello = "Hello";
        
        mvc.perform(get("/hello"))
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }
}

🐝 @Runwith(SpringRunner.class)

  • 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시킨다.
  • SpringRunner라는 스프링 실행자를 사용한다.
  • 즉, 스프링 부트 테스트와 JUnit 사이의 연결자 역할을 한다.

🐝 @WebMvcTes

  • 여러 스프링 테스트 어노테이션 중, Web에 집중할 수 있는 어노테이션이다.
  • 선언할 경우 @Controller, @ControllerAdvice 등은 사용할 수 있지만,
  • @Service, @Component, @Repository 등은 사용할 수 없다.

🐝 @Autowired

  • 스프링이 관리하는 빈(Bean)을 주입 받는다.

🐝 private MockMvc mvc

  • 웹 API를 테스트할 때 사용한다.
  • 스프링 MVC 테스트의 시작점
  • 이 클래스를 통해 HTTP GET, POST 등에 대한 API 테스트를 할 수 있다.

🐝 mvc.perform(get("/hello"))

  • MockMvc를 통해 /hello 주소로 HTTP GET 요청
  • 체이닝이 되어 아래와 같이 여러 검증 기능을 이어서 선언할 수 있음

🐝 .andExpect(status().isOk()

  • mvc.perform의 결과를 검증
  • HTTP Header의 Status를 검증 (200, 404, 500 ...)
  • 해당 코드에서는 OK, 즉 200인지를 검증

🐝 .andExpect(content().string(hello));

  • 응답 본문의 내용을 검증
  • 해당 코드에서는 리턴 값이 "Hello"가 맞는지 검증

코드를 다 작성하였으면, 메소드 왼쪽의 화살표를 클릭하여 메소드를 실행한다.


아앗! 야생의 오류가 발생하였다!


https://ddasi-live.tistory.com/35 을 참고하여 오류를 수정해주었다.
Settings > Build, Execution, Deployment > Build Tools > Gradle로 들어가 Run tests using을 IntelliJ IDEA로 변경해주었다.


프로젝트 첫 테스트코드 성공 냠냠굿 ^ㅅ^b

테스트 코드로 검증했지만, 아직도 의심이 된다 -.-^
이번에는 수동으로 실행해서 확인해보자.


포트 번호 체크!
http://localhost:8080/hello로 들어가봅시다

테스트 코드의 결과와 같은 것을 확인할 수 있다. 구웃 ~

브라우저로 한 번씩 검증은 하되, 테스트 코드는 꼭 작성해야 한다.

테스트 코드를 작성해야 견고한 소프트웨어를 만드는 역량이 성장할 수 있다.

추가로, 절대 수동으로 검증하고 테스트 코드를 작성하지 않는다.
테스트 코드로 먼저 검증한 후, 못 믿겠다면 프로젝트를 실행해 확인하자!


롬복 소개 및 설치

자바 개발자들의 필수 라이브러리 롬복(Lombok)을 알아보자!

롬복은 자바 개발을 할 때 자주 사용하는 코드인 Getter, Setter, 기본 생성자, toString 등을 어노테이션으로 자동 생성해 준다.


build.gradle에서 다음과 같이 의존성을 추가한 뒤 새로 고침을 한다.
build.gradle 파일을 수정하면 오른쪽에 귀여운 코끼리와 새로고침 버튼이 생긴다. 그것을 눌러주면 된다.

이제 롬복 플러그인을 설치한다.

읭? 플러그인 설치하려고 했는데 난 이미 설치되어있다네
설치가 안되었다면 설치를 하고 인텔리제이를 재시작하자!

그리고 Settings > Build, Execution, Deployment > Compiler > Annotation Processors로 들어가 Enable annotation processing을 체크해주자.

플러그인은 한 번만 설치하면 되지만, build.gradle에서 라이브러릴 추가하는 것과 Enable annotation processing을 체크해주는 것은 프로젝트마다 진행해주어야 한다.

이제 기존 코드를 롬복으로 리팩토링해보자.

Hello Controller 코드를 롬복으로 전환하기

먼저 web 패키지에 dto 패키지를 추가하자.
앞으로 모든 응답 DTO는 이 dto 패키지에 추가한다.

DTO : Data Transfer Object, 계층 간 데이터 교환을 위해 사용하는 객체
DAO : Data Access Object, 데이터베이스의 데이터에 접근하기 위한 객체, 데이터베이스에 접근하기 위한 로직 & 비즈니스 로직을 분리하기 위해 사용

HelloResponseDto를 생성하자.

package com.example.demo.web.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public class HelloResponseDto {
    private final String name;
    private final int amount;
}

🐝 @Getter

  • 선언한 모든 필드의 get 메소드를 생성해줌

🐝 @RequiredArgsConstructor

  • 선언된 모든 final 필드가 포함된 생성자를 생성
  • final이 없는 필드는 생성자에 포함되지 않음

final 키워드

  • 변수에 사용 - 변수 수정 불가
  • 메서드에 사용 - override를 제한
  • 클래스에 사용 - 상속 불가능 클래스가 됨

이제 이 DTO에 적용된 롬복이 잘 작동하는지 간단한 테스트 코드를 작성해보자.

package com.example.demo.web.dto;

import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class HelloResponseDtoTest {
    @Test
    public void 롬복_기능_테스트() {
        // given
        String name = "test";
        int amount = 1000;
        
        // when
        HelloResponseDto dto = new HelloResponseDto(name, amount);
        
        // then
        assertThat(dto.getName()).isEqualTo(name);
        assertThat(dto.getAmount()).isEqualTo(amount);
        
    }
}

🐝 assertThat

  • assertj 테스트 검증 라이브러리의 검증 메소드
  • 검증하고 싶은 대상을 메소드 인자로 받는다
  • 메소드 체이닝이 지원됨
    • isEqualTo와 같이 메소드를 이어서 사용

🐝 isEqualTo

  • assertj의 동등 비교 메소드
  • assertThat에 있는 값과 isEqualTo의 값을 비교해서 같을 때만 성공

책의 저자는 Junit의 기본 assertTat 대신 assertj의 assertThat을 사용했다.
Junit과 비교하여 assertj는,
  • CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않다.
    • Junit의 asserThat을 쓰게 되면 is()와 같은 CoreMatchers 라이브러리가 필요
  • 자동완성이 좀 더 확실히 지원됨


테스트 패스 완료 ^ㅅ^ bb

하지만...! 바로 되진 않았고 처음에
lombok error: variable name not initialized in the default constructor private final String name;라는 오류가 발생했다.
그 원인은 나의 gradle 버전이 5여서 그런거였다.
아래 링크를 참고하여 build.gradle을 수정하여 해결해주었다.

https://github.com/jojoldu/freelec-springboot2-webservice/issues/78

이제 HelloController에서도 responseDto를 사용해보자.

    @GetMapping("/hello/dto")
    public HelloResponseDto helloDto(@RequestParam("name") String name, @RequestParam("amount") int amount) {
        return new HelloResponseDto(name, amount);
    }

🐝 @RequestParam

  • 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션

이제 추가된 API를 테스트하는 코드를 추가하자.

    @Test
    public void helloDto가_리턴된다() throws Exception {
        String name = "hello";
        int amount = 1000;

        mvc.perform(
                get("/hello/dto")
                        .param("name", name)
                        .param("amount", String.valueOf(amount))
        )
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is(name)))
                .andExpect(jsonPath("$.amount", is(amount)));
    }

🐝 param

  • API를 테스트할 때 사용될 파라미터를 설정
  • String 값만 가능
    • 숫자/날짜 등의 데이터도 등록할 때는 문자열로 변경해야만 가능

🐝 jsonPath

  • JSON 응답값을 필드별로 검증할 수 있는 메소드
  • $를 기준으로 필드명을 명시


JSON이 리턴되는 API 역시 정상적으로 테스트를 통과한다!

profile
[~2023.04] 블로그 이전했습니다 ㅎㅎ https://leeeeeyeon-dev.tistory.com/

0개의 댓글