[Test] 단위 테스트와 통합 테스트, MSW
테스트 코드를 작성하는 이유
테스트 코드를 작성하다 보면 테스트를 가능하게끔 작성하다보니 코드를 간결하고 유연하게 작성하거나 의존성을 최소화 하는 방향으로 코드를 작성하게 된다.
⇒ 자연스럽게 유지보수성과 재사용성이 높아진다.
E2E 테스트란?
E2E 테스트는 End To End 테스트의 약자로 애플리케이션의 흐름을 처음부터 끝까지 테스트하는 것을 의미한다. 유닛 테스트나 통합 테스트는 모듈의 무결성을 증명할 수 있는 강력한 테스트이지만, 모듈의 무결성이 애플리케이션 동작의 무결성까지는 증명해 줄 수 없다.
E2E 테스트 과정에서는 실제 사용자의 시나리오를 테스트함으로써 애플리케이션 동작을 테스트하게 되고, 이 테스트를 통과함으로써 애플리케이션의 무결성을 증명할 수 있게 되는 것이다.
테스트 툴
최신 테스트 도구 중 가장 주목할만한 도구는 스토리북과 Cypress가 있다.
소프트웨어 관점에서 테스트를 정의한다면 "애플리케이션이 요구 사항에 맞게 동작하는지를 검증하는 행위" 정도가 될 것이다. 보통은 개발의 결과물이 최종적으로 사용자에게 전달되기 직전에 QA(Quality Assuarance)라는 과정을 거치는데, 이 과정을 테스트라고 보는 것이 일반적이다.
하지만 실제로 전체 개발 과정을 살펴보면 이러한 검증이 각 단계에서 꾸준히 이루어지는 것을 볼 수 있다. 예를 들어 프로토타입 과정에서 UX를 미리 검증하고 개선하는 일, 서버의 API를 호출하고 기대값을 확인하는 일, 마크업이 끝난 이후에 디자인 시안과 비교해보는 일 등이다. 사실은 이러한 모든 일도 테스트의 범주의 속한다고 볼 수 있다.
자동화 테스트의 중요성
테스트는 대부분 반복적인 작업으로 이루어진다. 이러한 반복된 작업을 매번 수동으로 진행하게 되면 테스트를 소홀히 하게 되며 결국 애플리케이션 품질의 저하로 이어진다.
또한 코드를 수정할 때마다 매번 관련된 기능을 테스트해야 하는 부담 때문에 코드 개선을 망설이게 되고, 이는 코드의 품질 또한 저하시킨다.
테스트 작업을 코드로 작성해서 자동화를 하게 되면 테스트에 대한 비용이 줄어들고, 테스트가 누락되거나 잘못 검증하는 등의 실수도 방지할 수 있다. 코드 수정에 대한 두려움이 없어져 적극적으로 리팩토링 등의 코드 개선을 할 수 있게 되고, 이는 곧 코드의 품질의 향상으로 이어지게 된다.
테스트의 기회비용
모든 테스트에 대해 자동화 테스트를 작성해야 하는 것은 아니다. 테스트 코드를 작성하고 유지 보수하는 데는 비용이 들기 때문이다. 그러므로 투입된 비용에 비해 얻는 효과가 적다면 차라리 수동으로 테스트하는 것이 더 낫다.
단위 테스트
단위 테스트(Unit Testing)는 애플리케이션의 가장 작은 단위인 개별 함수 또는 컴포넌트가 올바르게 작동하는지 확인하는 테스트이다.
❓단위 테스트 작성의 필요성
언제 단위 테스트가 유용할까?
1. 새로운 기능 추가 시: 새로운 기능을 추가하거나 기존 기능을 수정할 때 기존 기능이 의도대로 작동하는지 확인할 수 있다.
2. 버그 수정 시: 버그를 수정한 후 동일한 버그가 다시 발생하지 않도록 보장한다.
3. 코드 리팩토링 시: 코드 구조를 변경할 때 기능이 유지되는지 확인할 수 있다.
4. 문서화: 단위 테스트는 코드의 작동 방식을 이해하는 데 도움을 줄 수 있는 훌륭한 문서가 된다.
단위 테스트 라이브러리
단위 테스트 라이브러리로는 Jest와 React Testing Library가 있는데, CRA를 사용하면 기본적으로 설치되어 있기 때문에 별도로 설치할 필요는 없다.
✨Jest: 단위 테스트, 통합 테스트, 스냅샷 테스트 등을 지원하며, 쉽게 설정하고 사용할 수 있는 환경을 제공한다. (Vite를 사용하는 환경에서는 Vitest를 사용하기도 한다.)
- 빠른 테스트 실행: 테스트 실행 속도가 빠르며, 변경된 파일만 테스트하는 watch 모드도 지원한다. 이를 통해 개발 중에 빠르게 피드백을 받을 수 있다.
- 스냅샷 테스트: 스냅샷 테스트를 지원하여, 컴포넌트의 UI가 변경되었는지 쉽게 확인할 수 있다.
✨ React Testing Library: React 컴포넌트를 테스트하기 위한 라이브러리로, DOM을 기반으로 실제 사용자와 상호작용하는 것처럼 테스트를 작성할 수 있으며 사용자 중심의 테스트를 작성하는 데 중점을 두고 있다.
- Jest와 완벽하게 통합되어, Jest의 matcher와 assertion을 그대로 사용할 수 있다.
- 컴포넌트의 내부 구현보다는 외부 동작에 중점을 두어 테스트를 작성하므로, 리팩토링 시 테스트 코드의 변경을 최소화할 수 있다.
단위 테스트는 언제 필요한 것일까?
유틸리티 함수: 유틸리티 함수는 재사용성이 높고, 애플리케이션의 다양한 부분에서 사용되기 때문에 정확한 동작을 보장하기 위해 단위 테스트가 필요하다.
컴포넌트 상태 관리: 컴포넌트가 상태를 관리하는 경우, 상태가 올바르게 업데이트되고 렌더링되는지 확인하기 위해 단위 테스트가 필요하다.
테스트 코드 작성 기법
✨ Given-When-Then 기법
- Given: 테스트의 초기 상태나 조건을 설정
- When: 테스트 대상이 되는 행동을 수행
- Then: 행동의 결과를 확인
import { formatDate } from "./date";
// Given: 날짜 객체가 주어졌을 때
// When: formatDate 함수를 호출하면
// Then: yyyy-MM-dd 형식의 문자열이 반환
test("formatDate returns correct format", () => {
const date = new Date("2024-07-23");
// `expect`: 테스트의 결과를 검증하기 위한 Jest의 함수. 이 함수는 기대하는 값을 인자로 받아, 이를 기반으로 실제 결과와 비교한다.
// `toBe`: `expect` 함수와 함께 사용되어, 기대하는 값과 실제 값이 일치하는지 확인하는 Jest의 matcher 함수.
expect(formatDate(date)).toBe("2024-07-23");
});
✨ AAA Pattern
- Arrange : 테스팅 환경과 값을 정의함
- Act: 테스트 되어야할 코드를 실행함
- Assert : 실행 결과값을 평가함 / 예상되어야 하는 결과 혹은 값에 부합하는지 비교
it("유효하지 않는 숫자가 제공되면 NaN이 발생해야 함.", () => {
const input = ["invalid", 123];
const result = add(input);
expect(result).toBeNaN();
});
통합 테스트
cf. Mock
실제 객체를 만들기엔 비용과 시간이 많이 들거나 의존성이 길게 걸쳐져 있어 제대로 구현하기 어려울 경우, 가짜 객체를 만들어 사용하는데 이것을 Mock
이라 한다.
즉, Mock
은 테스트할 때 필요한 실제 객체와 동일한 모의 객체를 만들어 테스트의 효용성을 높이기 위해 사용한다.
MSW
MSW(Mock Service Worker)는 API Mocking 라이브러리로, 서버향의 네트워크 요청을 가로채서 모의 응답(Mocked response)을 보내주는 역할을 한다. 따라서, Mock Service Worker(MSW) 라이브러리를 통하면, Mock 서버를 구축하지 않아도 API를 네트워크 수준에서 Mocking 할 수 있다.
MSW를 통해 네트워크 Request에 대해 새로운 방식으로 Mocking이 가능해졌고, 색다른 개발 과정을 겪을 수 있게 되었다.
MSW가 이러한 역할을 할 수 있는 이유는 Service Worker를 통해 HTTP 요청을 가로채기 때문이다.
Service Worker
웹 애플리케이션의 메인 스레드와 분리된 별도의 백그라운드 스레드를 실행시킬 수 있는 기술이다. 네트워크 요청을 가로챔으로써 HTTP Request와 Response를 보고 캐싱 처리와 로깅 등 새로운 동작이 가능하다.
MSW 동작 원리
Service Worker를 브라우저에 설치하고 설치 이후로는 브라우저에서 실제 이루어지는 요청을 Service Workder가 가로채게 된다.
Service Worker에서는 실제 요청을 복사해서 MSW에게 해당 요청과 일치하는 모의 응답을 제공 받고 브라우저에게 그대로 응답을 전달하게 된다.
이러한 과정을 통해 실제 서버 존재 여부와 상관없이 예상할 수 있는 요청에 대해 Mocking이 가능해진다.
통합 테스트를 고려하여 함수, 컴포넌트를 작성하는 방법
- 단일 책임 원칙: 각 컴포넌트나 함수가 한 가지 역할만 하도록 작성한다. ⇒ 테스트 하기가 쉬워짐.
- 의존성 주입: 외부 의존성을 함수나 컴포넌트 내부에서 직접 호출하지 않고, 인자로 받아서 사용한다. ⇒ 테스트시 Mocking이 쉬워진다.
- 비동기 로직 분리: 비동기 로직을 별도의 함수로 분리하고, 컴포넌트에서 함수를 호출하도록 한다. ⇒ 비동기 로직 개별 테스트가 쉬워진다.
- 테스트 가능한 구조: 복잡한 로직을 작은 함수로 분리한다.
- 컴포넌트 계층 구조: UI와 비즈니스 로직을 분리하여 각각을 테스트하기 쉽게 하도록 한다.
참고 및 출처
https://fe-developers.kakaoent.com/2023/230209-e2e/