시작하면서
사실 이전 프리코스에서는 "다들 그렇게 하니까" 라는 생각으로 쫓기듯 코드를 작성했다. 하지만 이번 프리코스에서는 "내 코드를 완벽히 이해하자" 라는 목표를 세우고, 나만의 근거를 갖고 개발 방식을 선정했다. 그러는 와중 테스트 코드를 확실하게 정복해 보고 싶다는 욕심도 생겼다. 처음 도전해보는 방식들이 많겠지만, 하나의 미션을 가지고 여러 개발자 분들과 의견을 주고받을 수 있는 좋은 기회를 통해 이러한 방식을 깎아나가고 싶다!
배경
우테코 프리코스의 과제에는 이런 내용이 있다.
"기능 단위로 커밋"
기능 단위로 커밋하려면 어떻게 해야 할까? 내가 해당 기능을 제대로 구현했음을 어떻게 확신할 수 있을까? 테스트코드를 작성하면 된다. 실제로 과제 코드에도 이미 App 단위의 테스트코드가 작성되어 있었다.
기능을 다 구현하고 테스트코드를 작성했다고 해보자. 슬프게도 테스트가 실패하고 말았다. 어디서 잘못된 것일까? 우리는 이 원인을 찾기 위해 기능의 전체 코드를 뒤져야 한다. 그리고 문제를 해결하기 위해 설계 자체를 바꿔야 할지도 모른다.
이런 단점을 극복하기 위해, TDD를 도입해보기로 했다.
과정
TDD 시작하기
저번 프리코스가 끝나고 추천 도서 리스트에 있었던 <테스트 주도 개발> 을 학교 도서관에서 빌려왔다. 아래 내용을 바탕으로 미션 일부를 어떻게 해결했는지 기록하려고 한다.
우선 기본적으로 TDD를 위한 아래 과정을 지키려 노력했다.
1. 재빨리 테스트를 하나 추가
2. 모든 테스트 실행, 새로 추가한 테스트 실패 확인 [빨강]
3. 기능 구현
4. 모든 테스트 실행, 전체 테스트 성공여부 확인(일단 성공하게 만드는 것이 중요) [초록]
5. 리팩터링을 통한 중복 제거 [리팩터링]
빨강 ➡️ 초록 ➡️ 리팩터링
책에서 인상깊었던 유의점들
사실 책을 읽기 전에는 TDD라고 하면 기능 목록을 보고 모든 테스트를 쭉 작성하고, 그대로 기능을 구현해나가는 것을 떠올렸었다. 되돌아보면 이건 진짜 TDD의 T도 모르고 있는 것이었다. 책의 내용을 읽고 TDD가 어떤 것인지 감을 잡을 수 있었다.
- 자동화된 테스트를 구현하라. (자가 테스트 코드)
- 각각의 테스트는 다른 테스트와 완전히 독립적이어야 한다.
- 테스트를 한번에 다 만들어놓지 말자.
- 어디가 잘못되었는지 바로 파악할 수 있었다.
- 어떤 테스트부터 시작하는게 좋을까? 오퍼레이션이 아무 일도 하지 않는 경우를 먼저 테스트하자.
- 매우 간단한 기능은 한번에 기능까지 테스트했지만, 복잡한 기능은 실행 여부부터 테스트하며 단계적으로 구현하였다.
- 주제에 무관한 아이디어가 떠오르면 이에 대한 테스트를 할일 목록에 적어놓고 다시 주제로 돌아올 것
- 이전에는 코드를 작성하다가, 갑자기 예외 사항이 떠오르면 딴길로 새서 그 떠오른 예외 사항을 잊기 전에 구현하곤 했었다. 이번에는 이 유의점을 명심하고, 기능 목록에 추가만 한 뒤 하던 작업을 계속했다.
- 개인 프로젝트 시 프로그래밍 세션을 어떤 상태로 끝마치는 게 좋을까? 마지막 테스트가 깨진 상태로 끝마치는 것이 좋다.
- 특히 많은 도움이 되었다. 항상 "뭐 하려고 했더라..?" 라는 생각을 하는데, 이렇게 깨진 상태로 끝마치면 다시 시작할 때 바로 무엇을 해야할 지 알 수 있었다!
- 발생하기 힘든 에러 상황을 어떻게 테스트할 것인가? 실제 작업을 수행하는 대신 그냥 예외를 발생시키기만 하는 특수 객체를 만들어서 이를 호출한다.
- 아래 예시의 경우, 어떤 조건을 통해서 InvalidCustomSeparatorError를 발생시키려고 조작하는 것이 아니라, 그냥 그 에러 객체 자체를 던지도록 했다.
test("InvalidCustomSeparatorError 에러 발생 시", () => {
try {
throw new InvalidCustomSeparatorError();
} catch (error) {
expect(error.message).toBe(`${ERROR_HEADER} ${ERROR_BODY.INVALID_CUSTOM_SEPARATOR}`);
}
});
간단 예제로 TDD 소개
가장 간단한 출력 예시를 통해 TDD를 어떻게 진행했는지 소개하려고 한다.
기능 목록
- [] 그 외의 경우 정상값 출력
```bash
결과 : ${답}
```
1. 테스트를 하나 추가한다.
import Output from "../src/Output";
describe("printResult()", () => {
test("결과 출력", async () => {
const spy = jest.spyOn(Output, "printResult");
Output.printResult();
expect(spy).toBeCalled();
});
});
2. 테스트를 실행하고 추가한 테스트의 실패를 확인한다.
- 현재 아직 테스트하려는 메서드가 없기 때문에 당연히 테스트는 실패한다.
- 이때 나머지 테스트는 성공해야 한다.
3. 기능을 구현한다.
class Output {
/**
* 덧셈 결과를 출력합니다.
* @param {number} result - 덧셈 결과
*/
static printResult(result) { }
}
export default Output;
4. 전체 테스트 성공여부를 확인한다.
5. 리팩터링, 반복
- 이때 기능과 테스트에 중복된 내용이 있다면 리팩터링한다.
- 하나의 사이클이 끝났다. 이제 테스트의 내용을 구체화시키고, 다시 테스트를 실행하여 실패를 확인하고, 입력받은 값을 출력하는 기능을 구현하고, 전체 테스트 성공여부를 확인한다.
- 이때 커밋의 기준은 "현재 목표로 하고 있는 기능 목록의 체크박스를 채울 수 있는가" 로 지정하였다.
인생 첫 TDD 감상
아직은 익숙하지 않아서 리팩터링과 기능구현이 섞이기도 하고, 테스트 단위를 잘못 잡아서 갈아엎기도 하고, 반복되는 코드를 작성하기도 했다. 그리고 그냥 구현하는 것보다 많은 시간을 투자해야 했다.
하지만 정말 새로운 경험이었다. 테스트코드를 작성하는 것만으로 테스트 그 자체보다는 "설계"에 집중하게 되었다. 그리고 구현의 사이클이 정말 짧아지면서, 계속해서 작은 목표를 세우고 달성함으로써 더 재밌게 개발할 수 있었다. 또한 이전에 프로젝트에서 테스트 코드를 작성(정말 드물게)했을 때는, 이미 만들어진 기능에 짜맞춘다는 느낌을 떨칠 수 없었다. 이런 문제점 역시 TDD는 해결해줄 수 있었다.
이러한 개인적인 감상이 아니더라도 조직에도 긍정적인 영향을 미칠 것이다. 프리코스 미션은 여기서 끝일 수도 있지만, 만약 이 기능이 계속해서 유지보수해야 하는 기능이라면? 엄청난 효율을 가져올 것이라고 생각한다.
그 외 새로 배운 내용
에러 클래스
커스텀 클래스는 Error나 다른 내장 에러 클래스를 상속받아 만들 수 있습니다. 이때 super를 호출해야 한다는 점과 name 프로퍼티를 신경 써야 한다는 점을 잊지 마세요.
- 모던 자바스크립트 튜토리얼
// 자바스크립트 자체 내장 에러 클래스 Error의 '슈도 코드'
class Error {
constructor(message) {
this.message = message;
this.name = "Error"; // (name은 내장 에러 클래스마다 다릅니다.)
this.stack = <call stack>; // stack은 표준은 아니지만, 대다수 환경이 지원합니다.
}
}
내가 적은 기능 목록에는 세 가지의 다른 종류의 에러가 등장한다. 이 에러들을 따로 구분하여 던져주기 위해서 위 내용을 참조하여, 에러 클래스를 상속받은 커스텀 에러 클래스를 만들었다.
정규식에 표현식을 넣고 싶다면?
두 문법의 중요한 차이점은 /.../를 사용하면 문자열 템플릿 리터럴에서 ${...}를 사용했던 것처럼 중간에 표현식을 넣을 수 없다는 점입니다. 슬래시를 사용한 방법은 완전히 정적입니다.
- 모던 자바스크립트 튜토리얼
// Wrong
const regex = /`,|:|${customSeparator}`/;
const regex = /`${separators.join("|")}`/;
// Good
const regex = new RegExp(`,|:|${customSeparator}`);
const regex = new RegExp(separators.join("|"));
커밋 전 테스트, 커밋 메시지 검증으로 요구사항 지키기
- 처음에는 커밋 메시지 규칙을 scope 없이 설정하였는데, 나중에 수정하면서 엄청난 리베이스를 해야만 했다...
- 완전한 규칙을 정하고 시작하는 것이 얼마나 중요한 것인지 느낄 수 있었다. "저 이런것도 했어요" 라고 말하는 멋진 개발자가 되기 위함이 아니라 정말 실용적으로 필요한 일이다.
jest 제대로 활용해보기
사실 이전까지 jest의 문법을 어느정도 알고 사용했지만, 정확하게 숙지하지 못하고 대충 넘어가는 경우도 있었다. 이번 기회에 jest 공식 문서를 살펴보면서 필요한 문법들을 정리해 나가고 있다.
전체 회고
잘한 부분
TDD를 하려고 노력하다 보니, 확실히 코드가 기능별로 명확하게 모듈화된다는 것을 느꼈다. 또한 고민할 필요 없이 검증된 기능 단위로 커밋할 수 있었다.
아쉬운 부분 / 남은 궁금증
- 테스트 이름짓기 - 관련된 내용을 따로 찾아보지 않고 내가 보기 편하게 작성하였다.
- 사용하지 않는 테스트 주석처리
- ValidationError를 확장하여 여러 에러 클래스들을 따로 만들었는데, 이렇게 관리하는게 맞는지 확신이 들지 않았다.
- 클래스에 대한 이해 부족
- 어디까지 App 테스트를 해야 하는지에 대한 의문. 메서드 단위로 테스트하다 보면 전체 테스트 시 중복되는 부분이 당연히 생길 수밖에 없는데, 어디까지 테스트를 해야 할까?
어떻게 개선할 것인가?
- 테스트 네이밍 관련 내용 조사, 나만의 규칙 만들기
- 주석으로 처리하는 대신 사용할 수 있는 skip() 활용하기
- 에러 클래스 관리 관련 내용 학습
- 어떤 경우에 클래스가 필요한지 생각해보기
- TDD 관련 도서 남은 부분 읽고 정리하기
'프로젝트 > 우테코 프리코스' 카테고리의 다른 글
[우테코 프리코스 3주차 회고] 모델: 뷰야, 나 업데이트됐어! 새로 렌더링해조. (0) | 2024.11.06 |
---|---|
[우테코 프리코스 2주차 회고] 넌 전혀 MVC 하고 있지 않아 (1) | 2024.10.30 |