기록방

13장 : 테스트 코드 작성하기 본문

FrameWork/Spring

13장 : 테스트 코드 작성하기

Soom_1n 2024. 7. 9. 20:25

길벗 IT도서에서 주관하는 코딩 자율학습단 8기 : Spring Boot 파트에 참여한 기록입니다 [ 목록 ]

13.1 테스트란

  • 테스트(test)
    • 프로그램의 품질을 검증하는 것으로, 의도대로 프로그램이 잘 동작하는지 확인하는 과정
    • 초창기에는 사람이 직접 요청, 응답을 확인하는 방식으로 진행
    • 현재는 테스트 도구를 이용해 반복 검증 절차를 자동화
    • 다양한 문제를 미리 예방하고 코드 변경 등으로 인해 발생하는 부작용도 조기에 발견
  • 테스트 코드(test code)
    • 테스트 도구를 활용해 코드를 검증한다는 것은 테스트 코드를 작성해 실행한다는 말
    • 과정
      1. 예상 데이터 작성
      2. 실제 데이터 획득
      3. 예상 데이터와 실제 데이터 비교 검증
    • 테스트를 통과하면 지속적인 리팩토링으로 코드를 개선
    • 테스트를 통과하지 못하면 잘못된 부분을 찾아 디버깅(debugging)
  • 테스트 케이스(test case)
    • 다양한 경우를 대비해 작성한 테스트 코드
    • 성공뿐만 아니라 실패할 경우도 고려
  • 테스트 주도 개발(TDD, Test Driven Development)
    • 일단 테스트 코드를 만든 후 이를 통과하는 최소한의 코드부터 시작해 점진적으로 코드를 개선 및 확장해 나가는 개발 방식

13.2 테스트 코드 작성하기

13.2.1 테스트 코드 기본 틀 만들기

1. 테스트 생성 창 이동

index() 메서드 우클릭 → 생성(Generate) → 테스트(Test)

2. 테스트 생성 창

테스트 라이브러리는 JUnit5 고정. 멤버는 index():List<Article> 선택.

3. 테스트 코드

package com.example.firstproject.service;

import org.junit.jupiter.api.Test; // Test 패키지
import static org.junit.jupiter.api.Assertions.*; // 앞으로 사용할 패키지

class ArticleServiceTest {
    @Test // 해당 메서드가 테스트 코드임을 선언
    void index() {
    }
}
  • test > java > com.example.firstproject > service > ArticleServiceTest

4. 테스트 코드를 스프링 부트와 연동

package com.example.firstproject.service;

import org.junit.jupiter.api.Test; // Test 패키지
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*; // 앞으로 사용할 패키지

**// 스프링 부트와 연동해 통합 테스트 수행 선언 : 테스트 코드에서 스프링 부트의 객체 주입 가능**
**@SpringBootTest**
class ArticleServiceTest {
    **@Autowired
    ArticleService articleService;**
    @Test // 해당 메서드가 테스트 코드임을 선언
    void index() {
    }
}

13.2.2 index() 테스트하기

package com.example.firstproject.service;

import com.example.firstproject.entity.Article;
import org.junit.jupiter.api.Test; // Test 패키지
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*; // 앞으로 사용할 패키지

@SpringBootTest // 스프링 부트와 연동해 통합 테스트 수행 선언 : 테스트 코드에서 스프링 부트의 객체 주입 가능
class ArticleServiceTest {
    @Autowired
    ArticleService articleService;
    @Test // 해당 메서드가 테스트 코드임을 선언
    void index() {
        // 1. 예상 데이터
        Article a = new Article(1L, "가가가가", "1111");
        Article b = new Article(2L, "나나나나", "2222");
        Article c = new Article(3L, "다다다다", "3333");
        List<Article> expected = new ArrayList<Article>(Arrays.asList(a, b, c));

        // 2. 실제 데이터
        List<Article> articles = articleService.index();

        // 3. 비교 및 검증
        assertEquals(expected.toString(), articles.toString());
    }
}

테스트 통과

 

한글 깨짐 현상 해결 필요했음
help → 사용자 지정 VM 옵션 편집 → 최하단에 -Dfile.encoding=UTF-8 입력

13.2.3 show() 테스트하기

@Test
void show_성공_존재하는_id_입력() {
    // 1. 예상 데이터
    Long id = 1L;
    Article expected = new Article(id, "가가가가", "1111");

    // 2. 실제 데이터
    Article article = articleService.show(id);

    // 3. 비교 및 검증
    assertEquals(expected.toString(), article.toString());
}
@Test
void show_실패_존재하지_않는_id_입력() {
    // 1. 예상 데이터
    Long id = -1L;
    Article expected = null;

    // 2. 실제 데이터
    Article article = articleService.show(id);

    // 3. 비교 및 검증
    assertEquals(expected, article);
}
  • 성공과 실패 경우를 나누어 테스트

13.2.4 create() 테스트하기

@Test
void create_성공_title과_content만_있는_dto_입력() {
    // 1. 예상 데이터
    String title = "라라라라";
    String content = "4444";
    ArticleForm dto = new ArticleForm(null, title, content);
    Article expected = new Article(4L, title, content);

    // 2. 실제 데이터
    Article article = articleService.create(dto);

    // 3. 비교 및 검증
    assertEquals(expected.toString(), article.toString());
}

@Test
void create_실패_id가_포함된_dto_입력() {
    // 1. 예상 데이터
    Long id = 4L;
    String title = "라라라라";
    String content = "4444";
    ArticleForm dto = new ArticleForm(id, title, content);
    Article expected = null;

    // 2. 실제 데이터
    Article article = articleService.create(dto);

    // 3. 비교 및 검증
    assertEquals(expected, article);
}

13.2.5 여러 테스트 케이스 한 번에 실행하기

전체 테스트 코드를 실행했는데 하나 틀림

필요:[Article(id=1, title=가가가가, content=1111), Article(id=2, title=나나나나, content=2222), Article(id=3, title=다다다다, content=3333)]
실제:[Article(id=1, title=가가가가, content=1111), Article(id=2, title=나나나나, content=2222), Article(id=3, title=다다다다, content=3333), Article(id=4, title=라라라라, content=4444)]
  • index()의 로그를 보면 필요 데이터는 3개인데, 실제 데이터는 4개가 들어있음
  • 앞선 테스트인 create_성공_title과_content만_있는_dto_입력() 에서 데이터가 추가되었는데 삭제하지 않았기 때문
  • 이러한 문제를 해결하기 위해 트랜잭션으로 처리해서 테스트 하나가 끝나면 롤백
    • 조회를 제외하고 생성, 수정, 삭제는 모두 @Transactional 를 붙여주어야 함

잘 성공함

🚀 1분 퀴즈

  • 다음 빈칸에 들어갈 용어를 찾아 쓰세요
    • ( 테스트 케이스 ) : 프로그램의 다양한 상황을 자동으로 검증하기 위한 테스트 코드
    • ( 디버깅 ) : 프로그램에서 발생한 문제의 원인을 찾고 고치는 작업
    • ( 리팩토링 ) : 프로그램의 수행 결과는 그대로 유지하면서 코드의 구조 및 성능을 개선하는 작업
    • ( @SpringBootTest ) : 스프링 부트 환경과 연동된 테스트를 위한 애노테이션
    • ( @Transactional ) : 테스트를 수행해 조작된 데이터를 복구하는 애노테이션

✅ 셀프 체크

본문의 ArticleServiceTest에 다음 5가지 테스트 케이스 코드를 추가로 작성하세요.

1. updated()를 성공한 경우 1

@Test
@Transactional
void update_성공_존재하는_id와_title_content가_있는_dto_입력() {
    // 1. 예상 데이터
    Long id = 1L;
    String title = "수정 제목";
    String content = "수정 내용";
    ArticleForm dto = new ArticleForm(id, title, content);
    Article expected = new Article(id, title, content);

    // 2. 실제 데이터
    Article article = articleService.update(id, dto);

    // 3. 비교 및 검증
    assertEquals(expected.toString(), article.toString());
}

2. updated()를 성공한 경우 2

@Test
@Transactional
void update_성공_존재하는_id와_title만_있는_dto_입력() {
    // 1. 예상 데이터
    Long id = 1L;
    String title = "수정 제목";
    ArticleForm dto = new ArticleForm(id, title, null);
    Article expected = new Article(id, title, "1111");

    // 2. 실제 데이터
    Article article = articleService.update(id, dto);

    // 3. 비교 및 검증
    assertEquals(expected.toString(), article.toString());
}

3. updated()를 실패한 경우

@Test
@Transactional
void update_실패_존재하지_않는_id의_dto_입력() {
    // 1. 예상 데이터
    Long id = -1L;
    ArticleForm dto = new ArticleForm(id, null, null);
    Article expected = null;

    // 2. 실제 데이터
    Article article = articleService.update(id, dto);

    // 3. 비교 및 검증
    assertEquals(expected, article);
}

4. delete()를 성공한 경우

@Test
@Transactional
void delete_성공_존재하는_id_입력() {
    // 1. 예상 데이터
    Long id = 1L;
    Article expected = new Article(id, "가가가가", "1111");

    // 2. 실제 데이터
    Article article = articleService.delete(id);

    // 3. 비교 및 검증
    assertEquals(expected.toString(), article.toString());
}

5. delete()를 실패한 경우

@Test
@Transactional
void delete_실패_존재하지_않는_id_입력() {
    // 1. 예상 데이터
    Long id = -1L;
    Article expected = null;

    // 2. 실제 데이터
    Article article = articleService.delete(id);

    // 3. 비교 및 검증
    assertEquals(expected, article);
}

 

전체 성공

🏓 더 알아볼 내용

1. TDD

TDDTest Driven Development의 약자로, 미국의 소프트웨어 엔지니어인 켄트 벡(Kent Beck)이 주창한 개발 방법론입니다. 테스트 주도 개발이라고 하는데요. 테스트를 먼저 작성하고 메인 코드는 그다음에 작성하는 개발 방법론입니다.

TDD 사이클은 정말 간단합니다.

  1. 실패하는 테스트를 작성합니다. 메인 코드가 없어도 좋아요. 없으면 실패하겠죠.
  2. 성공하는 테스트를 작성합니다. 테스트가 성공만 하면 돼요. 원하는 동작까지 가지 못해도 돼요.
  3. 리팩토링 합니다. 원하는 동작에 가까워지도록 수정합니다.

위 3가지 동작을 무한 반복하는 것이 바로 TDD입니다.

1) TDD의 장점

  • 보다 정확한 요구 사항에 집중할 수 있습니다.
  • 다양한 예외 케이스를 생각하며 코드를 작성할 수 있습니다.
  • 디버깅 시간을 단축할 수 있습니다.
  • 재설계 시간을 단축할 수 있습니다.
  • 보다 유연한 구조로 코드를 작성할 수 있습니다.

2) TDD의 단점

TDD는 장점도 많지만 단점도 명확합니다.

  • 테스트를 작성하는 데 많은 시간이 소요됩니다.
  • 익숙하지 않을 경우 생산성이 많이 떨어집니다.

그렇기 때문에 테스트를 좋아해도 TDD로 개발하는 것은 쉽지 않습니다. 제가 이전에 테스트에 관해 작성한 글이 있는데, 한 번 읽어보길 추천 드립니다.

2. @SpringBootTest

테스트의 종류는 통합 테스트, 단위 테스트, E2E 테스트가 있습니다.

  • 통합 테스트: 서버를 실행시켜 실제 환경과 유사한 상황에서 테스트하는 것을 말합니다.
  • 단위 테스트: 테스트하고 싶은 메서드만 테스트하는 것을 말합니다.
  • E2E 테스트: 서버의 경우 HTTP 응답을 테스트하는 것을 말합니다.

통합 테스트, 단위 테스트, E2E 테스트의 차이에 대해 알고 싶다면 다음 문서를 참고해 주세요.

@SpringBootTest는 통합 테스트에 사용되는 애노테이션입니다.

@SpringBootTest의 코드를 살펴보죠.

위 문서에는 @SpringBootTest 어노테이션이 ContextLoader, Environment, webEnvironment 등 다양한 환경을 제공한다고 되어 있습니다. 심지어 Rest API 요청을 테스트하기 위해서 TestRestTemplate, WebTestClient도 제공한다고 합니다.

예시를 들어볼까요? 다음은 간단한 테스트 코드로, TaskService 클래스에 있는 createTask()라는 메서드를 테스트하고 있습니다.

@SpringBootTest // 1 ..... 통합 테스트
@Transactional // 2 .... 테스트 완료 후 db 롤백을 위한 어노테이션
class TaskServiceTest {
    @Autowired
    private TaskRepository taskRepository; // 3 ... TaskRepository 의존성 주입
    @Autowired
    private TaskService taskService; // 4 ... TaskService 의존성 주입
    @Test
    void createTask() {
        TaskCreateRequest taskCreateRequest = new TaskCreateRequest("test title");
        Task task = taskService.createTask(taskCreateRequest);
        assertThat(task.getId()).isNotNull();
        assertThat(task.getTitle()).isEqualTo("test title");
    }
}

위 코드에서 주석이 붙은 행의 의미는 다음과 같습니다.

  1. @SpringBootTest 애노테이션을 통해 통합 테스트를 할 것이라고 선언했습니다.
    • 앞서 코드에서 봤듯이 ContextLoader를 통해서 SpringContext를 가져오게 될 것입니다.
    • Spring은 Bean을 Context에서 관리합니다.
  2. 이러한 테스트 코드에서도 @Transactional 애노테이션을 사용할 수 있는데요.
    테스트를 완료한 후 DB에 저장된 데이터를 롤백하기 위해 사용합니다.
  3. @Autowired 애노테이션을 통해 SpringContext에 있는 Bean과 필드를 묶어줍니다.

@SpringBootTest는 강력합니다. TaskRepository 객체를 사용자가 직접 생성하지도 않아도 되고, TaskService 객체도 사용자가 직접 생성하지 않아도 됩니다. @SpringBootTest가 SpringContext에서 가져와 알아서 사용합니다.

🥕 멘토 TIP

  • TDD에 대해서 공부하고 싶다면 <테스트 주도 개발> 책을 꼭 읽어보길 바라요.
  • 테스트를 많이 작성하면 테스트를 잘 작성하기 위해서 메인 코드를 더 유연한 구조로 작성하게 됩니다.
  • 물론 테스트를 위해서 코드가 너무 잘게 쪼개지는 것은 지양해야 해요.
728x90