Today What I Learned
매일 배운 것을 이해한만큼 정리해봅니다.
오늘은 리액트에서 tesing-library를 통해 페이지 렌더 테스트를 만드는 과정을 공부해보았습니다.
오랜만에 회사에서 페어 프로그래밍을 했는데 오늘은 웹페이지를 ts로 변경하고, 페이지 렌더 테스트까지 작성했다. 유닛 테스트는 작성해보았지만 페이지 렌더 쪽 테스트 작성은 이번이 처음이라(ts도 아직 모지리지만) 페어분이 몇가지 포인트로 알려주신 테스트 작성 방식과 고려점을 좀 정리해보려고 한다.
블로깅에 앞서 Special Thanks to today's pair, Moon.
1. Testing Library
1. Testing Library이란?
Simple and complete testing utilities that encourage good testing practices
- 좋은 테스팅 습관을 길려주는 간단하고 완벽한 테스팅 유틸리티
- 오늘도 시작은 홈페이지에 definition부터, 이들이 말하는 좋은 테스팅 습관은 무엇일까?
테스트 방법론의 종류는 매우 다양하지만 오늘 언급한 테스팅 라이브러리는 아래 문제를 해결하고자 존재한다.
2. When to User Testing Library
- ui 개발 중 마주치게 되는 문제들
- 개발을 하다보면 비즈니스 로직 뿐 아니라 UI를 테스트하고 싶을 때가 있다. 그러나 UI가 담는 모든 디테일을 실어 나르면서 테스트하는 방법은 피하고 싶다. 의도한 지점들을 간파하는 테스트만 만들어 내는 방법은 없을까?
- 기존 개발에 변화를 줄 수 있는 요소들이 있을 것이다. 이런 요소들이 이미 정성 들여 작성해둔 테스트 코드를 망가트리지 않았으면 한다. 또 이런 요소들 때문에 개발자의 개발 속도를 잡아먹지 않았으면 한다.. 이런 점들을 감안해서 유지보수 가능한 코드를 작성할 순 없을까?
- 위 문제를 해결하기 위해선 테스팅 라이브러리는 개발한 코드를 구현하기 위한 모든 필요 요소를 갖추지 않고도 매우 가볍게 테스트를 작성할 수 있게 만들어졌다. 유저들이 어플리케이션에서 ui를 발견할 수 있는 것처럼 노드구조에서 무언가를 불러와서(query) 발견하는 구조로 작성되었다고 한다.
2. Testing Library로 Page 테스트 작성하기
- 프론트엔드에서도 생각보다 다양한 테스트를 구현할 수 있다.
- 유닛테스트로 클라이언트에 존재하는 비즈니스 로직을 테스트할 수도 있고
- 스토리북과 같은 툴을 사용해서 컴포넌트 단위로 ui/ux를 고려하며 테스트할 수 있다.
- 또 사이프레스와 같은 툴을 사용해 실제 사용자가 클라이언트를 다루는 것처럼 플로우를 작성하여 e2e 테스트를 진행해볼 수도 있다.
- 혹은 페이지를 단위로 하는 컴포넌트 렌더 테스트도 필요할 수 있다.
- 오늘 내가 페어프로그래밍 한 것도 마지막 맥락이었는데, 테스팅 라이브러리가 제공하는 툴들을 이용해서 리액트 페이지 렌더와 페이지 내 유의미한 액션이 기대한 바대로 작동하는지를 테스트하는 방법을 좀 더 살펴보겠다.
1. 테스팅 기본 개념
- 테스트 시에는 의존적인 상황이 아니라 고립된 상황에서의 구현을 확인해 볼 필요가 있다. 그래서 페이지 혹은 컴포넌트에서 작성한 method나 util 함수를 대신하여 테스트 오브젝트를 생성하여 활용하곤 하는데 이 때 Stub과 Mock이라는 개념을 적용한다.
- Stub과 Mock은 모두 dummy obejct를 반환한다. 그러나 아래와 같은 차이점이 있다.
- Stub: 어떠한 비즈니스 로직에 기반하여 항상 정해진 결과를 리턴하도록 object를 짜는 방법, 즉 상황에 따라 어떤 로직에 기반해 항상 정해진 더미 객체를 반환하는 형태이다.
- Mock: 모킹 또한 정해진 결과를 항상 리턴하지만, 사용자 임의의로 액션을 조작할 수 있다. 예를 들어 성인인지를 확인하는 method를 앞두고 "15세 혹은 20세" 등의 더미값을 리턴하는 식이라고 볼 수 있다. Mock은 Stub과 대조되는 개념이 아니라 좀 더 상위의 개념이라고 볼 수 있다.
- 좀 더 자세한 내용을 보려면 quora의 질의응답을 봐도 좋을 것 같다.
2. 페이지 테스트 절차와 고려점
- 나는 이번에 이미 작성되어 있는 페이지의 테스트 코드를 작성하였기 때문에 기존 페이지 컴포넌트를 대조하면서 테스트를 작성하였다. 궁극적으로는 기본적인
<div>Page </div>
기본 구조부터 테스트와 함께 작성하는 것을 목표로 하면 좋을 것 같다.
1. 무엇부터 작성할 것인가
- 페이지 외부에서 주입하는 것들을 파악: Provider, props
- 먼저 페이지를 그리기 위해 페이지 외부에서 받아와야 하는 것을 정의하고 구현한다.
- 페이지 외부에서 받아오는 것들은 대표적으로 Provider로 주입되는 정보들과 props들이 있을 것이다.
- redux, mobx 같은 state management를 사용한다면 이 라이브러리들이 HOF 개념으로 컴포넌트에 store 정보를 주는 것을 알 수 있다. 이번 테스트에서는 단독으로 페이지를 그리기 때문에 페이지 바로 상단에 Provider로 정보를 주입해주어야만 한다.
- 나 같은 경우는 페이지 컴포넌트에서 사용하는 mobx store 일부를 찾고 해당 store에서 사용하는 value property와 method를 정의하였다.
- 아래와 같은 컴포넌트에서는 loading, route와 같은 props와 sessionManager, userManager 등의 store 주입이 있다.
import React from "react";
import { Redirect } from "react-route-dom";
import { useStores } from "../utils/hooks";
import { Photo } from "./Photo";
import { Loading } from "../common/Loading";
const MyPage: React.FC = ({loading, route: {disableFooter}}) => {
const {sessionManager, userManager } = useStores();
...
const handleFetchPhotos = id => {
return userManager.fetchPhoto().then(({id}) => {
});
}
if(!sessionManager.signIn) {
return <Redirect to="/home" />;
}
if(loading) {
return <Loading />
}
return (
<div data-test="my_page">
<Photo onFetchPhotos={handleFetchPhotos}/>
</div>
);
}
export default MyPage;
- 주입한 store와 props에서 컴포넌트에 사용되는 프로퍼티나 메소드를 stub/mock의 개념을 빌어 더미로 구현한다.
- 앞서 테스팅 라이브러리에서 언급한 해결점 중 하나가 ui 테스트 상에서 필요로 하는 모든 정보를 구현하지 않은 채 기능하는지를 체크하는 테스트를 작성하게 하는 것!이었다.
- 이를 위해 페이지가 활용하고 있는 모든 값을 다 넣지 않고도 faker와 같은 툴을 이용해 dummy data를 간단하게 만든다.
- 대부분 이러한 행위는 test/it을 사용하여 단위별 테스트를 작성하기 전인 beforeEach 단계에서 작성하고, 각 테스트가 마주하는 상황에 독립성을 확보해주어야 한다.
- beforeEach 단계에서의 작성 방식은 단순 모킹으로 구현하여도 되고, msw와 같은 npm 라이브러리를 사용할 수도 있다.
- 테스트이긴 하지만 실제로 서버로 api 통신을 통해서 값을 받아오는 과정이 존재할 수도 있기 때문에 msw를 활용하여 클라이언트 -> 서버 간 통신 사이 프록시 서버를 만들고 서버단의 api response를 모킹하여서 클라이언트로 돌려주는 식으로 테스트를 작성할 수도 있다.
- 아래와 같은 식으로 스텁을 구현하고 리액트 테스트 라이브러리에서 제공하는 render, configure 등의 메소드를 가져와서 ui 상의 변경을 테스트로 정의한다.
import faker from "faker";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { render, configure } from "@testing-library/react";
...
describe("My Page Test", () => {
const route = {
disableFooter: false,
};
const server = SetupServer();
let stores = {};
beforeEach(() => {
server.listen();
sotres = {
sessionManager: {
signedIn: true,
},
userManager: {
fetchUserInfo: () => ({
id: faker.random.number();
userCode : faker.random.word();
}),
fetchPhoto: ({id: "12345"}) => Promise.reolve();
}
};
});
afterEach(() => {
jest.clearAllMocks();
});
afterAll(() => {
server.close();
});
it("페이지를 렌더한다", async() => {
const {debug, getByTestId} = render(<Provider {...stores}/><MyPage route={route}/></Provider>);
});
debug();
expect(queryByTestId("my_page")).toBeInTheDocument();
});
function SetupServer() {
return setupServer(
rest.get("/api/myInfo.json", (req, res, ctx) => {
return res(ctx.json({userCode: "1234qwerasdf"}));
}),
);
}
}
- 그런 다음 렌더 후 페이지 내 유의미한 행동들이 실행되고 있는지에 대한 테스트 문을 짠다. 여기서부터는 유닛 테스트와 크게 다르지 않고, ui 단의 변경까지 테스트에서 감지하고 싶은 경우 위에서와 같이 getText 등으로 ui 상 변경점을 expect문으로 체크하면서 테스트를 작성하면 된다.
(이미 글이 길어져서 이 뒤 테스트 작성 방식은 향후 포스트에서 예시를 적어보는 것으로)
2. 페이지 테스트 작성 시 주의점
- 어떤 컴포넌트도 그렇겠지만 특히 페이지 테스트의 경우 무한 루프를 돌게하는 로직이 없는지 체크해야 한다.
- 위에서 작성한 것처럼 페이지를 구현하기 위한 더미 데이터를 이용해 먼저 1)렌더 시키고 2)렌더 된 형태에서 특정 유의미한 기능이 작동하는지를 확인하는 것이 테스트의 목적이다.
- 이 때 1)이 완료되지 않고 계속해서 무한루프에 빠져들면 테스트의 목적을 잃게 되기 때문이다.
- 따라서 render 단의 조건절이 테스트에서도 잘 구현되어 있는지를 보아야 한다.