B-1. UI 테스트 작성 — 테스트를 위해 프로덕션 코드를 바꿔도 될까?
이렇게 읽어보세요: 선배 크루의 고민 → 토론 질문을 먼저 읽고 → AS-IS 코드를 직접 분석 → 리뷰어의 피드백으로 관점 확인 → 탐색 미션에서 PR 깊이 파기
선배 크루의 고민
“테스트를 통과시키려고 프로덕션 코드에 optional chaining(
?.)을 추가했는데, 이게 맞는 건지 찝찝합니다. 테스트 환경에서 DOM이 없어서 null이 되는 걸 우회한 건데…” — 하루, PR #33
“추가하다가 문득… 테스트를 구현하기 위해
LottoList의private method를public으로 변경해야겠다고 생각이 들었는데, 이런 경우 public으로 메소드를 풀어야 하나요? 아니면 다른 방식이 또 있을까요?” — 준찌, PR #121
“calculateRankHistory()는 내부에서만 사용되지만, 테스트 때문에 어쩔 수 없이 public으로 놔뒀습니다. 궁금한 점은 테스트만을 위해 특정 메서드를 public으로 유지하는 경우도 있나요? 아니면 더 좋은 방법이 있을까요?” — 기린, PR #356
이 코드를 보면서 이야기해봅시다
- “
getLottoRateOfReturn()은 수익률을 계산하는 순수한 로직인데, 테스트하려면 DOM이 필요한 구조입니다. 어떻게 분리하면 DOM 없이 테스트할 수 있을까요?” - “optional chaining은 문제를 해결한 걸까요, 숨긴 걸까요?”
AS-IS 코드
출처: PR #33 — 하루 (2021, 2단계) · 해당 파일
// components/ResultModal.js — 테스트 통과를 위해 프로덕션 코드에 optional chaining을 추가
export default class ResultModal {
constructor({ isVisible, lottoTickets, winningNumber, onRestart }) {
this.$modal = $('.modal');
this.$modalClose = $('.modal-close');
this.attachEvents();
}
attachEvents() {
// 테스트 환경에서 DOM이 없어서 null → optional chaining으로 우회
this.$modalClose?.addEventListener('click', this.closeModal.bind(this));
this.$resetButton?.addEventListener('click', () => {
this.onRestart();
this.closeModal();
});
}
// 이 메서드를 테스트하고 싶지만, 생성자에서 DOM을 찾으므로
// 테스트 환경에서는 에러가 발생한다
getLottoRateOfReturn() {
const profit = this.lottoTickets.reduce(
(acc, lottoTicket) => acc + WINNING_PRIZE[lottoTicket.totalMatchCount].PRIZE,
0
);
const loss = this.lottoTickets.length * LOTTO_PRICE;
return getRateOfReturn(profit, loss);
}
}리뷰어의 피드백
“테스트를 위해 프로덕션 코드에 trick이 가해지는 상황은 없어야 합니다!” — PR #33 (링크 )
“도메인 폴더의 퍼블릭 메소드와 constants를 이용해보세요. 테스트에서 private 메서드에 접근할 필요가 없도록 설계하는 것이 중요합니다.” — PR #121 (링크 )
“테스트 코드를 위해 접근 제어자를 변경하는 것은 지양해야 합니다. public 인터페이스를 통해 테스트할 수 있도록 구조를 고민해보세요.” — PR #356 (링크 )