포도가게의 개발일지
[Tech] TDD? 본문
책: 테스트 주도 개발 시작하기
chapter1.
저자가 TDD 도입전 개발했던 방식 ( 현재의 나와 같다. )
1. 만들 기능에 대해 설계를 고민, 어떤 클래스나 인터페이스를 도출할지 고민하고 각 타입에 어떤 메서드를 넣을지 시간을 들여 생각
2. 과정 1을 수행하면서 구현에 대해서 고민, 대략 어떻게 구현하면 될지 머릿속에 그림
3. 기능에 대한 구현을 완료할 것 같으면 기능을 테스트 한다. 이 과정에서 원하는 대로 동작하지 않거나 문제가 발생하면 과정2에서 작성한 코드를 디버깅하면서 원인을 찾는다.
저자는 TDD가 더 나은 코드를 다 빠르게 개발하는데 도움이 된다고 한다.
TDD로 개발할 대 먼저 해야할 것은 기능을 검증하는 테스트 코드를 작성하는 것이다. TDD를 처음에 익히기 위해 작은 단계를 차근차근 밟아 나가야한다. 가령 return 3;을 고정해버려 첫 가장 작은 테스트를 pass 할 수 있을지라도, 그 이후에 다른 검증 코드를 작성하여 코드를 리팩토링 하자. 책에서는 test 폴더 내에 작업한 class가 모든 test를 통과하게 되면 배포될 폴더에 포함시킨다.
기능을 검증하는 테스트 코드를 먼저 만든다. 이 과정에서 테스트 대상이 될 클래스 이름, 메서드 이름, 파라미터 개수, 리턴 타입을 고민한다. 또한 새로운 객체를 생성할지 아니면 정적 메서드로 구현할지 등을 함께 고민한다. 이런 고민 과정은 실제 코드를 설계하는 과정과 유사하다. 메서드 이름이나 이런것을 정할 때 구글 검색을 통해 적합하고 뚜렷한 단어를 선택하길..
첫번째 테스트의 선정은 중요하다. 잘 선택하지 못하면 이후 진행 과정이 순탄하게 흘러가지 않는다. 첫 번째 테스트를 선택할 때는 가장 쉽거나, 가장 예외적인 상황을 선택해야 한다. 이 때 한번에 만들어야 할 코드가 많아지면 지양한다. 사실상 구현을 다하고 테스트하는 것과 다를바 없다.
TDD는 테스트를 통과시킬 만큼의 코드를 작성한다.!!
chapter2.
1. 테스트 코드의 중복을 무턱대고 제거하면 안된다. 중복을 제거한 뒤에도 테스트 코드의 가독성이 떨어지지 않고 수정이 용이한 경우에만 중복을 제거해야 한다.!!!, 제거 후 오히려 테스트 코드 관리가 어려워지면 중복을 rollback해야한다.
2. TDD 사이클은 레드-그린-리팩터로 부리기도 한다. 여기서 레드는 실패하는 테스트를 의미한다. 그린은 성공한 테스트를 의미한다. 리팩터를 리팩토링을 의미한다.
3. 테스트 코드를 만들면 다음 개발 범위가 정해진다. 테스트 코드가 추가되면서 검증하는 범위가 넓어질수록 구현도 점점 완성되어 간다. 이렇게 테스트가 개발을 주도해 나간다.
4. 구현을 완료한뒤에는 리팩토링을 진행했다. 하지만 리팩토링할 대상이 눈에 들어오지않으면 다음 테스트를 진행했다. 당장 리팩토링 하지 않더라도 테스트 코드가 있으면 리팩토링을 보다 과감하게 진행할 수 있다.
테스트 코드 작성순서
1. 쉬운 경우에서 어려운 경우로 진행
2. 예외적인 경우에서 정상인 경우로 진행 : 초반부터 다양한 조합을 검사하는 복잡한 상황을 테스트로 추가하면 해당 테스트를 통과시키기 위해 한번에 구현해야 할 코드가 많아진다.!!!!
chapter3.
- TDD를 시작할 때 테스트할 목록을 미리 정리하면 좋다. 이렇게 테스트할 내용을 정리했다면, 이중에 어떤 테스트가 구현이 쉬울지 상상해본다. 또는 어떤 테스트가 예외적인지 상상해본다. 시간을 조금 들여서 구현의 난이도나 구조를 검토하면 다음 테스트를 선택할 때 도움이 된다.
- 리팩토링 범위가 크면 리팩토링에 실패할 수도 있다. 그러니 범위가 큰 리팩토링을 진행 하기 전에는 코드를 커밋하는 것을 잊지말자.
TDD?
- TDD란 Test Driven Development의 약자로 ‘테스트 주도 개발’이라고 한다.
- 반복 테스트를 이용한 소프트웨어 방법론으로 작은 단위의 테스트 케이스를 작성하고 이를 통과하는 코드를 추가하는 단계를 반복하여 구현한다.
- 짧은 개발 주기의 반복에 의존하는 개발 프로세스이며, 애자일 방법론 중 하나인 eXtream Programming(XP)의 ‘Test-First’ 개념에 기반을 둔 단순한 설계를 중요시한다.
단위 테스트(unit Test)란?
말 그대로 한 단위(일반적으로 class)만을 테스트하는 것이다.
Why?
- 피드백과 협력을 증진시키기 때문에 불확실성에 대해 대비를 하게 해준다.
피드백
- TDD를 하면 피드백이 증가한다.
- 테스트를 통과하는 것으로 잘되고 있는가를 자주 확인할 수 있다.
협력
- 공유하면 협력이 증진된다.
- 남이 짠 코드를 빨리 이해할 수 있다.
- 남이 짠 코드를 쉽게 이해할 수 있다.
- 용기가 생긴다.(내가 남의 코드를 고쳐서 문제가 있더라도 자동화된 테스트가 알려주기 때문에 큰 걱정없이 고칠 수 있는 용기가 생긴다.)
- 내가 이 코드를 고쳤을 때 이것을 망치면 어떡하지..
- 내가 그 사람의 의도를 모르는데 이것을 고쳐도 될까..
- 테스트 코드에는 개발자의 개발 과정(어떤 고민/어떤 의사결정)이 나와있기 때문이다.
TDD는 어떤 상황에서 해야할까(불확실성이 있을때)?
- 만약 어떤 부분에 대한 코딩을 여러번 해봤고 결과가 어떻게 나올지 뻔하다면 TDD를 하지 않아도 된다.
- 또한 TDD를 했을 때 얻는 것이 적다면 TDD를 하지 않아도 된다.
- 처음해보는 프로그램 주제
- 나에 대한 불확실성이 높은 경우
- 고객의 요구조건이 바뀔 수 있는 프로젝트
- 외부적인 불확실성이 높은 경우
- 개발하는 중에 코드를 많이 바꿔야 된다고 생각하는 경우
- 내가 개발하고 나서 이 코드를 누가 유지보수할지 모르는 경우
즉, 불확실성이 높을 때 TDD를 하면 된다.
TDD 개발 방식
how?
<Red>단계에서는 실패하는 테스트 코드를 먼저 작성한다.
<Green>단계에서는 테스트 코드를 성공시키기 위한 실제 코드를 작성한다.
<Yellow>단계에서는 중복 코드 제거, 일반화 등의 리팩토링을 수행한다.
중요한 것은 실패하는 테스트 코드를 작성할 때까지 실제 코드를 작성하지 않는 것과, 실패하는 테스트를 통과할 정도의 최소 실제 코드를 작성해야 하는 것이다. 이를 통해, 실제 코드에 대해 기대되는 바를 보다 명확하게 정의함으로써 불필요한 설계를 피할 수 있고, 정확한 요구 사항에 집중할 수 있다.
package com.example.firstproject.service;
import com.example.firstproject.dto.ArticleForm;
import com.example.firstproject.entity.Article;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.transaction.Transactional;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class ArticleServiceTest {
private ArticleService articleService;
@Autowired
public ArticleServiceTest(ArticleService articleService){
this.articleService = articleService;
}
@Test
void show_success() {
Long id = 1L;
String title = "test";
String content = "test";
Article expected = new Article(id, title, content);
Article actual = articleService.show(id);
assertEquals(expected.toString(), actual.toString());
}
@Test
void show_fail() {
Long id = -1L;
Article actual = articleService.show(id);
assertEquals(null, actual);
}
@Test
@Transactional
void create_success() {
String title = "tdd";
String content = "tdd_test";
ArticleForm dto = new ArticleForm(null,title,content);
Article actual = articleService.create(dto);
assertEquals(title, actual.getTitle());
assertEquals(content, actual.getContent());
}
@Test
@Transactional
void create_fail() {
String title = null;
String content = "tdd_test";
ArticleForm dto = new ArticleForm(null,title,content);
Article actual = articleService.create(dto);
assertEquals(null, actual);
}
@Test
@Transactional
void delete_success() {
Long id = 1L;
String title = "test";
String content = "test";
Article expected = new Article(id, title, content);
Article actual = articleService.delete(id);
assertEquals(expected.toString(), actual.toString());
}
@Test
@Transactional
void delete_fail() {
Long id = -1L;
Article actual = articleService.delete(id);
assertEquals(null, actual);
}
}