스냅샷에서 사용자 행동으로: React Testing Library로 전환하기
일반적인 프로젝트에서는 테스트가 마크업에 집중하는 경우가 많습니다. 스냅샷 테스트는 DOM 상태를 캡처하지만, 리팩터링 시 즉시 깨집니다. CSS 클래스 변경, 속성 순서 조정, 렌더링 최적화 등 작은 변화도 테스트 실패를 유발합니다. 실제로 발생하는 로직 오류는 여전히 감지되지 않습니다.
Enzyme로 작성한 문제 있는 테스트 예시
describe("UserProfileCards", () => {
it("should render", () => {
const wrapper = shallow(
<Provider store={store}>
<UserProfileCards items={mockedUserProfiles} />
</Provider>
)
expect(wrapper).toMatchSnapshot()
})
})
이 테스트는 컴포넌트의 동작에 대한 실질적인 신뢰를 주지 못합니다. 구조만 확인할 뿐, 사용자가 실제로 경험하는 바는 전혀 반영되지 않습니다.
구현 결합: 잘못된 성공 신호
내부 구현 세부 사항에 의존하는 테스트는 두 가지 문제를 야기합니다. 리팩터링 시 실패하고, 실제 버그는 놓칩니다.
CollapseIcon 컴포넌트가 opened 상태를 가진다고 가정해 봅시다:
import CollapseIcon from "./CollapseIcon"
import { shallow } from "enzyme"
describe("CollapseIcon", () => {
it("should render", () => {
const wrapper = shallow(<CollapseIcon opened={false} />)
expect(wrapper).toMatchSnapshot()
})
it("should react to closed", () => {
const wrapper = shallow(<CollapseIcon opened={false} />)
expect(wrapper.find(".collapse__icon").hasClass("collapseIcon_closed")).toEqual(true)
})
it("should react to opened", () => {
const wrapper = shallow(<CollapseIcon opened={true} />)
expect(wrapper.find(".collapse__icon").hasClass("collapseIcon_opened")).toEqual(true)
})
})
문제점:
- 스냅샷은 애니메이션 로직 오류를 감지하지 못함.
- 스타일 리팩터링 시 클래스 이름 검사가 깨짐.
- 애니메이션이 멈춰 있어도 테스트는 통과됨.
다른 사례: 지역화 상수에 강하게 결합된 테스트:
it("should render no-answers button", () => {
const button = getButton(mockCommentsList[0])
const noAnswersText = `${mockedTranslation.t(`${tNamespace}no-answers`)}
expect(button.text()).toBe(noAnswersText)
})
tNamespace 내 키를 변경하면 테스트가 깨지지만, 버튼 텍스트는 정확할 수 있습니다. 해결책: 상수를 컴포넌트와 테스트 모두가 가져오는 공유 모듈로 이동하세요.
모호한 테스트 설명은 상황을 더 악화시킵니다:
- "should show tooltip" — 어떤 조건인지 명시 없음.
- "should render without tooltipLabel" — 기대 결과 없음.
React Testing Library: 사용자 행동 테스트하기
RTL은 패러다임을 완전히 바꿉니다. 테스트는 실제 사용자의 행동을 시뮬레이션하고 내부 구현을 무시합니다. 상태, props, 메서드에 접근할 수 없으며, 오직 보이는 UI만 다룹니다.
RTL의 장점:
- 사용자 중심: 클릭, 입력, 화면 독백 같은 실제 사용자 행동을 테스트.
- 리팩터링 안전: 클래스 이름, ID, 내부 상태와 무관.
- API:
render(),screen.getByRole(),fireEvent.click(),waitFor(). - 비동기 지원: 데이터 로딩에
findByRole()활용 가능. - 접근성 강화:
getByLabelText()로 올바른 라벨링 보장.
RTL은 내구성을 위해 설계되었습니다. 테스트는 DOM 구조가 아니라 실제 사용 방식을 반영합니다.
고품질 테스트를 위한 체크리스트
테스트 작성 시
- 목적 명확화: 무엇을 테스트하는지 분명히 정의 (예: "isTooltipShow=true일 때 툴팁 표시")
- 사용자 관점: 실제 사용자가 하는 행동처럼 (예:
getByRole('button'),fireEvent) - 격리성: API는 모킹; 테스트 간 의존성 제거
- 설명: 코드 없이도 이해 가능한 전체 문장으로 작성
- 커버리지: 정상 경로 + 극단적 케이스 + 에러 상태 포함
테스트 검토 시
- 목표가 명확한가?
- 구현 세부사항(클래스, 상태)은 피했는가?
- 격리되어 있는가?
- 설명은 정보 제공이 되는가?
- 주석은 건설적인가?
팀 전환을 위한 조직화 전략
마이그레이션 워크숍 개최: 문제 있는 테스트 모아 분석, RTL로 전환. 추천 사항:
- 45분 단위로 세션 나누고, 실습 중심으로 진행.
- 페어 프로그래밍으로 함께 리팩터링.
- 이론보다 실습을 우선시.
레거시 마이그레이션: 새 테스트는 RTL로 작성, 점차 기존 테스트 재작성. 에어비앤비 사례: LLM 기반 파이프라인을 활용해 6주 만에 3,500개 파일 리라이팅 (75% 자동화).
핵심 요약
- 사고방식 전환: 마크업에서 사용자 행동으로.
- RTL vs Enzyme: RTL은 내부 접근 차단; Enzyme은 이를 유도.
- 체크리스트: 작성 및 검토 필수.
- 워크숍: 대화가 일방적 설명보다 효과적.
- 마이그레이션: 자동화는 속도를 높이지만, 철학적 이해가 필수.
— Editorial Team
아직 댓글이 없습니다.