TOC
참고
본 내용은 단위 테스트의 기술을 읽고 정리한 내용입니다.
책의 내용과 함께 개인적인 의견과 생각, 학습을 담아 작성하였습니다.
가독성의 요소
- 단위 테스트 이름 짓기
- 변수 이름 짓기
- 검증(assert)과 실행(action) 분리
- 초기화 및 설정 해제
하나씩 살펴보자
단위 테스트 이름 짓기
테스트가 포함된 파일 구조에는 다음 세 가지 중요한 정보가 포함되어야 한다.
- 작업 단위의 진입점(혹은 현재 테스트 중인 기능 이름)
- 진입점을 테스트하는 상황
- 작업 단위의 종료점이 실행해야 하는 동작
테스트 이름에서 이 정보들을 포함하면 범위, 기능, 역할 등을 쉽게 알 수 있다. 예를 들어 '진입점 X를 null값으로 호출하면, Y를 실행한다.' 형식으로 작성한다.
위의 세 가지 요소는 테스트를 읽는 사람이 쉽게 볼 수 있는 곳에 있어야 한다.
- 하나의 테스트 함수 이름에 포함시키기
- 중첩된 describe 블록을 사용해서 나누기
- 단순히 문자열 설명을 매개변수나 주석으로 사용하여 테스트에 포함시키기
사용하는 언어와 가독성
역자 - 영어를 써라
역자: "테스트 이름은 사용하는 언어에 따라 가독성이 달라진다."
- 언어마다 배치와 읽는 방향이 다르다.
- 영어는 동사가 먼저 나오지만, 한국어는 맨 뒤에 나온다.
- 중요한 동사가 먼저 나오는 게 명확하다.
과연 그럴까
저의 의견은 조금 다릅니다.
역자의 의견을 완전히 반박하기는 힘듭니다. 하지만, 저는 상황에 따라 달라질 필요가 있지 않을까 싶습니다. 만약 통일감을 위해 영어와 한국어 중 하나를 선택해야 한다면, 저는 한국어를 선택하겠습니다.
한국인 팀에서는 영어가 제 1언어가 아니기 때문에 개인마다 영어의 수준이 다를 수 있겠죠. 영어 해석 절차가 필요하다면 이 자체로 가독성을 해칠 수 있다고 봅니다.
저는 한국어가 명확하다고 봐요. 단, 이것 역시 역자처럼 저의 의견입니다. 비교의 기준은 '동사가 먼저 온다는 이점이, 한국어를 영어로 씀으로써 떨어트리는 가독성보다 더 클 것인가' 일 것 같네요.
한국어 쓸 거면 이렇게 해라
역자는 한국어를 쓸 것이라면 이렇게 하라고 합니다.
// describe() 블록을 사용하지 않는 경우
it("진입점 X를 null값으로 호출하면, 작업 단위의 종료점에서 Y가 수행되어야 한다.", () => {
// ...
});
// describe() 블록을 사용하는 경우
describe("진입점 X를 테스트할 때", () => {
describe("null 값으로 호출하면", () => {
it("Y를 수행해야 한다.", () => {
// ...
});
});
});
명심할 점
"이름을 명확히 지어라."
- 코드 가독성을 높여라.
- 다른 개발자가 테스트 이름만으로도 무엇을 테스트하는지 알게 하라.
- 파이프라인에서 실패했을 때 테스트 이름이 보인다.
- 특별한 디버깅 없이 오류 로그만으로 원인을 알 수 있다.
매직 넘버와 변수 이름
용어 설명
- 매직 넘버: 하드코딩된 값, 기록에 남지 않은 값, 명확하게 이해되지 않는 상수나 변수
- 매직: 값들이 동작하지만 이유를 알 수 없다. 마치 마법처럼.
매직 넘버 사용
describe("password verifier", () => {
test("on weekends, throws exceptions", () => {
expect(() => verifyPassword('jhGGu78!', [], 0))
.toThrowError("...")
})
})
이 코드를 보면 함수의 인수들이 각자 무슨 의미인지 알겠는가. 0
은 요일을 의미한다. []
는 비밀번호 유효성을 검증하는 규칙의 배열이다. jhGGu78!
은 비밀번호다. 아무 비밀번호. 그런데 정말 아무 비밀번호일지 알 수 있겠는가?
이런 테스트는 결국 구현부를 까봐야 한다. 이렇게 수정해보자.
유의미한 변수 사용
describe("password verifier", () => {
test("on weekends, throws exceptions", () => {
const ANY_PASSWORD = 'anything';
const SUNDAY = 0;
const NO_RULES = [];
expect(() => verifyPassword(ANY_PASSWORD, NO_RULES, SUNDAY))
.toThrowError("...")
})
})
- 변수의 이름과 값은 중요한 것을 설명하는 역할도 한다.
- 하지만, 코드를 읽는 사람이 어떤 부분을 신경 쓰지 않아도 되는지 알려 주는 역할도 한다.
저의 의견
이 부분은 테스트 뿐만 아니라, 어떤 코드를 작성할 때에든 적용되는 원리인 것 같습니다. 테스트를 안 짜더라도 더 다짐하고 가보겠습니다.
검증과 실행 단계 분리
가독성을 높이려면 검증 단계와 실행 단계를 한 문장에 넣지 말라.
// 좋지 않은 예시
expect(verifier.verify("any value")[0]).toContain("fake reason");
// 좋은 예시
const result = verifier.verify("any value");
expect(result[0]).toContain("fake reason");
- 한 줄에 너무 많이 담지 마라.
- 디버깅에도 더 좋다.
초기화 및 설정 해제
- 단위 테스트에서 초기화(setup)과 해제(teardown) 함수는 남용되기 쉽다.
- 해제 작업의 가독성 감소(ex. 초기 설정 및 테스트 후 목 초기화 등)
- 특히 초기화 함수에서 더 그렇다.
beforeEach 함수에서 목 초기화
- 설정 함수에서 목과 스텁을 설정하면 테스트 내에서는 해당 객체를 어디서 만들었는지 찾을 수 없다.
- 테스트를 읽는 사람은 모의 객체의 사용 여부나 역할을 알지 못할 수 있다.
- 초기화와 테스트 함수의 거리가 멀수록 더 그렇다.
해결
- 목은 테스트 내에서 직접 초기화하고 모든 기댓값을 설정한다.
- 유지보수성을 위해 목 생성 팩토리 함수를 만든다.
- beforeEach 같은 초기화 함수는 전혀 사용하지 않아도 된다.
정리
기억하고 싶은 좋은 말
장의 초입에서 나온 이야기인데, 좋아서 적어봅니다.
- 가독성은 테스트를 작성한 사람과 몇 달 후 이를 읽어야 하는 사람을 이어 주는 연결고리이다.
- 테스트는 프로젝트에서 다음 세대 프로그래머에게 들려 주는 이야기이다.
미래 세대와의 좋은 연결고리를 만들기 위해, 당장 테스트는 안 짜더라도 가독성 있는 코드를 짜봐야 겠네요.
중요 내용 되새김
- 변수의 이름과 값은, 코드를 읽는 사람이 어떤 부분을 신경 쓰지 않아도 되는지 알려 주는 역할도 한다.
- 검증 단계와 실행 단계를 한 문장에 넣지 말라. 그냥 한 줄에 뭘 많이 넣지 말라.
- 초기화 함수에서 목과 스텁 생성하지 말라. 테스트 자체에서 시작하라.
- 유지보수성을 위해 목 생성 팩토리 함수를 만들어라.
총평
- 테스트를 작성하는 차원이 아니더라도 도움이 되는 내용이었어요.
- 가독성과 유지보수성을 위한다면, 동작의 정의와 실행부의 거리가 멀면 안 된다는 걸 계속 강조하는 것 같아요. 초기화 함수와 테스트의 거리가 멀수록 추적이 어렵다고 하죠.
- 그리고 저자의 팩토리 함수 사랑도 다시 알 수 있었어요. 그런데 그 효과가 이제는 조금 피부로 느껴지는 듯 해요. 저자의 외침이 닿는 듯 합니다.
- 매직 넘버의 사용을 우아하게 해결하는 것도 좋았어요. 역시나 테스트가 아니더라도 안고 가야 할 기본 스킬들입니다.