테스트
테스트는 작성한 코드가 잘 작동하는 것을 검증하는 작업이다.
개발자로서 코드를 작성해서 브라우저에서 수동적으로 테스트할 수도 있지만, 이렇게 수동적인 테스트는 오류가 발생하기 쉽고 모든 시나리오를 테스트하기 어렵다.
따라서 우리는 자동화된 테스팅을 하는 것이 중요하다. 테스트 자동화는 추가적인 코드를 작성해서 애플리케이션의 메인 코드를 테스트하는 것을 의미한다. 전체 애플리케이션을 자동으로 테스트하는 코드를 작성하기 때문에 항상 모든 것을 테스트할 수 있다는 것이 장점이다.
테스트의 종류
유닛 테스트
- 컴퓨터 프로그래밍에서 소스 코드의 특정 모듈이 의도된 대로 정확히 작동하는 지 검증하는 가장 작은 단위의 테스트이다.
- 즉, 모든 함수와 메소드에 대한 테스트 케이스를 작성하는 절차를 의미한다.
통합 테스트
- 각각의 시스템들이 서로 어떻게 상호작용하고 제대로 작동하는 지 테스트하는 것을 의미한다.
- 단위 테스트 이후, 각 모듈들의 상호 작용이 제대로 이루어지는지 검증한다. 모듈을 통합하는 과정에서 발생할 수 있는 오류를 찾는 테스트이다.
E2E 테스트(End-To-End)
- 애플리케이션의 흐름을 처음부터 끝까지 테스트하는 것을 의미한다.
- 유닛테스트나 통합 테스트는 모듈의 무결성을 증명할 수 있지만, 모듈의 무결성이 애플리케이션 동작의 무결성까지는 증명해줄 수 없다.
- 실제 사용자의 시나리오를 테스트함으로써 애플리케이션 동작을 테스트하고, 이 테스트를 통과함으로써 애플리케이션의 무결성을 증명한다.
리액트에서 테스트하기
첫 번째로, 테스트 코드를 실행하고 결과를 확인하기 위한 도구가 필요한데, 보통 jest를 사용한다.
두 번째로, 리액트 앱과 컴포넌트를 시뮬레이팅하는(렌더링) 방법이 필요하다. 이것은 주로 react testing library를 사용한다.
테스트하려는 파일의 이름은 보통 [테스트하려는 컴포넌트 이름].test.js 로 짓는다.
테스트를 작성할 때는 세 가지 단계를 거칠 수 있다.
1. 테스트 환경을 설정한다. (테스트하고자 하는 컴포넌트를 렌더링한다.)
2. 테스트할 로직을 실행한다. (execute)
3. 예상한 결과와 실제 실행 결과를 비교한다.
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders Hello World as a text', () => {
render(<Greeting />);
const helloWorldElement = screen.getByText('Hello World!');
expect(helloWorldElement).toBeInTheDocument();
});
test 함수의 첫 번째 인수로 테스트에 대한 설명을 넣어주고, 두 번째 인수로는 실제 테스트할 코드를 포함한 함수를 넣어준다.
위에서 언급한 세 가지 단계를 통해 테스트를 작성해보면 다음과 같다.
1. 테스트할 Greeting 컴포넌트를 render 함수로 렌더링한다.
2. 여기서는 없다.
3. 가상 화면에 액세스할 수 있게 해주는 screen을 통해 화면에서 Hello World! 텍스트를 찾고 해당 요소가 존재하는지 확인한다.
이때, render 메서드는 전달한 컴포넌트 트리 전체를 렌더링한다.
즉, Greeting 컴포넌트 안에 있는 다른 컴포넌트를 무시하는 것이 아니라 그 컴포넌트들의 콘텐츠도 렌더링한다.
테스트 코드를 다 작성하면 npm run test로 테스트를 실행할 수 있다.
다른 예로 버튼을 클릭하는 이벤트가 있다고 생각해보자.
import userEvent from '@testing-library/user-event';
test('renders "Changed!" if the button was clicked', () => {
render(<Greeting />);
const buttonElement = screen.getByRole('button');
userEvent.click(buttonElement);
const outputElement = screen.getByText('Changed!');
expect(outputElement).toBeInTheDocument();
});
1. 컴포넌트 렌더링
2. getByRole 함수를 통해 화면에서 버튼을 가져오고, userEvent를 사용해 클릭 이벤트를 실행한다.
userEvent는 실제 화면에서 사용자 이벤트를 작동시키도록 돕는 객체이다.
3. 텍스트가 화면에 있는지 확인한다.
만약 어떤 요소가 화면에 없어야 한다면 get 메서드를 사용하면 안되고 query 메서드를 사용해야 한다.
const outputElement = screen.queryByText('good to see you', { exact: false, });
expect(outputElement).toBeNull();
element가 화면에서 찾아지지 않는다면 get메서드는 실패해 오류를 낼 것이고, query메서드는 null을 반환한다.
따라서 element가 null인지 확인하는 toBeNull 메서드로 확인하면 된다.
테스트 그룹화하기
애플리케이션의 규모가 커질수록 다수의 테스트들을 서로 다른 테스트 suite에 넣어 그룹화한다.
애플리케이션 내의 하나의 특징 또는 하나의 컴포넌트에 속하는 테스트들은 한 테스트 suite에 들어간다.
describe 함수를 통해 테스트 suite를 생성하면 된다.
describe('Greeting component', () => {
test('renders Hello World as a text', () => {
render(<Greeting />);
const helloWorldElement = screen.getByText('Hello World!');
expect(helloWorldElement).toBeInTheDocument();
});
test('renders "good to see you" if the button was NOT clicked', () => {
render(<Greeting />);
const outputElement = screen.getByText('good to see you', { exact: false });
expect(outputElement).toBeInTheDocument();
});
});
첫 번째 인수로 서로 다른 테스트들이 어디에 속할지 관한 카테고리 설명을 넣어주면 되고, 두 번째 인수로는 다른 테스트들을 넣은 함수를 작성하면 된다.
비동기 코드 테스트
return (
<div>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
컴포넌트 내의 useEffect에서 HTTP 요청을 보내서 posts를 가져온다고 해보자.
테스트 코드를 작성할 때 <li> 아이템들이 있는지 확인해서 posts를 제대로 가져왔는지 다음과 같이 확인할 수 있다.
describe('Async component', () => {
test('renders posts if request succeeds', () => {
render(<Async />);
const listItemElements = screen.getAllByRole('listitem');
expect(listItemElements).not.toHaveLength(0);
});
});
하지만 이 테스트는 실패한다. 그 이유는 get메서드는 screen의 아이템들을 즉시 가져오기 때문이다.
따라서 초기 렌더 사이클에는 요청이 전송되기 전이어서 posts가 빈 배열이기 때문에 listitem들이 없는 것이다.
이 방법을 해결하기 위해 find 메서드를 사용하면 된다. find 메서드들은 Promise를 반환하기 때문에 HTTP 요청이 성공할 때까지 기다릴 수 있다.
describe('Async component', () => {
test('renders posts if request succeeds', async () => {
render(<Async />);
const listItemElements = await screen.findAllByRole('listitem');
expect(listItemElements).not.toHaveLength(0);
});
});
테스트 코드를 async 함수로 작성해서 Promise를 반환하고, jest는 이 테스트가 끝날 때까지 기다리게 된다.
mock function
하지만 지금 이 테스트는 최선이 아니다.
보통 테스트를 실행할 때 서버에 HTTP 요청을 전송하지 않기 때문인데, 그 이유는 다음과 같다.
1. 많은 요청이 존재하게 된다면 네트워크 트래픽으로 인해 서버가 과부하될 것이다.
2. 데이터를 가져오는 것이 아닌 POST 요청을 전송하는 컴포넌트가 있다면 테스트로 인해 서버의 내용이 변경될 수 있다.
따라서 보통 실제 요청을 전송하지 않거나 테스팅 서버로 요청을 전송해야 한다.
테스트를 작성할 때는 "내가 작성하지 않은 코드"를 테스트 해서는 안된다. 예를 들어 fetch 함수가 제대로 작동하며 요청을 보내는지를 테스트하면 안된다. fetch는 브라우저 내장함수이지 내가 작성한 코드가 아니기 때문이다.
대신 전송된 요청의 결과에 따라 컴포넌트가 작동하는지 테스트하면 된다.
따라서 fetch 함수를 mock 함수로 대체해야 하는 것인데, 우리가 원하는 바를 수행하면서도 진짜 요청을 전송하지 않는 더미 함수를 쓰는 것이다.
그렇게 되면 테스트 중에 컴포넌트가 실행될 때 실제 함수가 아닌 mock 함수가 사용될 것이다.
describe('Async component', () => {
test('renders posts if request succeeds', async () => {
window.fetch = jest.fn();
window.fetch.mockResolvedValueOnce({
json: async () => [{ id: 'p1', title: 'First post' }],
});
render(<Async />);
const listItemElements = await screen.findAllByRole('listitem');
expect(listItemElements).not.toHaveLength(0);
});
});
window.fetch = jest.fn() -> jest.fn()으로 mock 함수를 만들어 fetch 함수를 mock 함수로 덮어쓴다.
이제 mock함수를 사용해 mockResolvedValueOnce 라는 특수 메서드를 호출할 수 있다. 이 메서드는 fetch 함수가 호출되었을 때 resolve 되어야 하는 값을 설정할 수 있게 해준다.
fetch(url)
.then((response) => response.json())
fetch 함수는 위와 같은 형태로 resolve되므로 여기서 response가 바로 mockResolvedValueOnce에서 설정하려는 값이 된다.
이렇게 mock함수로 덮어쓴 fetch가 호출되었을 때 Promise가 반환해야 하는 실제 값을 설정할 수 있다.
mock함수로 대체해서 성공 케이스를 테스트하고, 실제 API에 요청을 전송하고 있지 않기 때문에 과부하가 걸리는 일도 생기지 않게 된다.
'React' 카테고리의 다른 글
[React Native] 화면 크기와 플랫폼에 따라 다른 코드 작성하기 (0) | 2024.07.08 |
---|---|
[React+TypeScript] useRef로 DOM에 접근하기 (1) | 2023.10.04 |
[React] TanStack Query - 데이터 전송하기, Optimistic Update (0) | 2023.10.01 |
[React] TanStack Query (React-Query) (0) | 2023.09.30 |
[React] lazy loading (0) | 2023.09.28 |