회사의 기존 제품에 테스트 프레임워크 및 라이브러리를 도입하고 CI/CD 자동화까지 하는 프로젝트를 시작하였다. 먼저 TDD 테스트 주도 개발에 대해 알아보고 테스트 종류에는 어떤 것들이 있는지, 어떤 프레임워크/라이브러리가 도입하기 좋고 범용적으로 쓰이는지 조사해보았다.
// RTL
import { fireEvent } from '@testing-library/react';
fireEvent.change(검색창, { target: { value: '테스트 방법' } });
fireEvent.click(검색버튼);
// cypress
it('greets', () => {
cy.visit('app.html')
cy.get('#name').type('Cypress{enter}')
cy.contains('#answer', 'Cypress')
})
const response = await fetch('https://api.com', {
method: 'POST',
body: JSON.stringify({
혼인: {
남편: '크리스',
아내: '피터'
},
}),
});
const 결과 = await response.json();
expect(결과).toBe("결혼");
// mocking
jest.spyOn(window, 'fetch'); // fetch 함수 mocking
window.fetch.mockImplementation({
json: async () => '결혼',
});
var message;
beforeEach(() => message = 'Vue');
test('message equals to be Vue', () => {
expect(message).toBe('Vue');
});
test('message equals to be Vue!!', () => {
expect(message + '!!').toBe('Vue');
});
1. Setup/Teardown
React component tree를 document의 DOM elements에 붙여 렌더링하여 테스트. 테스트 후에는 document로 부터 component tree를 언마운트하여 종료한다.
import { unmountComponentAtNode } from "react-dom";
let container = null;
beforeEach(() => {
// DOM 엘리먼트를 렌더링 대상으로 설정
container = document.createElement("div");
document.body.appendChild(container);
});
afterEach(() => {
// 종료시 정리
unmountComponentAtNode(container);
container.remove();
container = null;
});
2. act()
리액트 react-dom/test-utils에서 제공
act 함수 안에 "unit"단위로 렌더링, 이벤트, 데이터 fetching을 진행한다. act 내부의 코드를 간결하게 하고 싶을 때 React Testing Library를 사용
3. 렌더링
컴포넌트 렌더링이 제대로 되었는지 테스트 예제
// hello.js
import React from "react";
export default function Hello(props) {
if (props.name) {
return <h1>Hello, {props.name}!</h1>;
} else {
return <span>Hey, stranger</span>;
}
}
// hello.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Hello from "./hello";
let container = null;
beforeEach(() => {
test(renders with or without a name", () => {
act(() => {
render(<Hello />, container);
});
expect(container.textContent).toBe("Hey, stranger");
act(() => {
render(<Hello name="Jenny" />, container);
});
expect(container.textContent).toBe("Hello, Jenny!");
act(() => {
render(<Hello name="Margaret" />, container);
});
expect(container.textContent).toBe("Hello, Margaret!");
});
4. Data Fetching
실제 API 호출하는 대신 dummy 데이터로 테스트. 아니면 end-to-end 프레임워크(Cypress, Playwright,Puppeteer)를 통해서 전체 app이 제대로 동작하는지 확인
// user.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import User from "./user";
let container = null;
test("renders user data", async () => {
const fakeUser = {
name: "Joni Baez",
age: "32",
address: "123, Charming Avenue"
};
jest.spyOn(global, "fetch").mockImplementation(() =>
Promise.resolve({
json: () => Promise.resolve(fakeUser)
})
);
// resolved promises를 적용하려면 `act()`의 비동기 버전을 사용하세요.
await act(async () => {
render(<User id="123" />, container);
});
expect(container.querySelector("summary").textContent).toBe(fakeUser.name);
expect(container.querySelector("strong").textContent).toBe(fakeUser.age);
expect(container.textContent).toContain(fakeUser.address);
// 테스트가 완전히 격리되도록 mock을 제거하세요.
global.fetch.mockRestore();
});
5. Moking Modules
Data Fetching이랑 비슷하게 dummy 모듈 만들어서 테스트 할 수 있다.
// contact.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Contact from "./contact";
import MockedMap from "./map";
jest.mock("./map", () => {
return function DummyMap(props) {
return (
<div data-testid="map">
{props.center.lat}:{props.center.long}
</div>
);
};
});
6. 이벤트
DOM 요소에 실제 DOM 이벤트를 전달하여 결과를 검증
// toggle.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import Toggle from "./toggle";
let container = null;
beforeEach(() => {
// 렌더링 대상으로 DOM 엘리먼트를 설정합니다.
document.body.appendChild(container);
});
it("changes value when clicked", () => {
const onChange = jest.fn();
act(() => {
render(<Toggle onChange={onChange} />, container);
});
// 버튼 엘리먼트를 가져와서 클릭 이벤트를 트리거 하세요.
const button = document.querySelector("[data-testid=toggle]");
expect(button.innerHTML).toBe("Turn on");
act(() => {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
expect(onChange).toHaveBeenCalledTimes(1);
expect(button.innerHTML).toBe("Turn off");
act(() => {
for (let i = 0; i < 5; i++) {
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
}
});
expect(onChange).toHaveBeenCalledTimes(6);
expect(button.innerHTML).toBe("Turn on");
});
7. 타이머
jest timer Mocks 사용
https://jestjs.io/docs/timer-mocks
8. 스냅샷 테스트
렌더링 된 컴포넌트 출력을 저장하고 제대로 변경사항이 적용되는지 테스트
// hello.test.js, again
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
import pretty from "pretty";
import Hello from "./hello";
it("should render a greeting", () => {
act(() => {
render(<Hello />, container);
});
expect(
pretty(container.innerHTML)
).toMatchInlineSnapshot(); /* ... gets filled automatically by jest ... */
FE 개발의 경우 UI/UX 관련 기능을 실제 사용자 환경과 분리된 상태에서 테스트하기에는 한게가 있기 때문에, 사용자의 관점에서 테스트를 할 수 있는 필요가 있음
Cypress
Playwright
Puppeteer
Testcafe
Webdriverio
https://ko.reactjs.org/docs/testing.html
https://jestjs.io/
https://tecoble.techcourse.co.kr/post/2021-10-22-react-testing-library/
https://velog.io/@binimini/Mocha-vs-Jest-JS-Test-Framework
https://dev.to/bonnie/testing-react-a-converts-journey-from-enzyme-to-testing-library-5ai0
https://youtu.be/3LMmPXoGI9Q
https://youtu.be/pkYUcKWOqPs
https://blog.mathpresso.com/모던-프론트엔드-테스트-전략-1편-841e87a613b2
https://ui.toast.com/fe-guide/ko_TEST
https://npmcompare.com/compare/cypress,playwright,puppeteer,testcafe,webdriverio
https://storybook.js.org/tutorials/ui-testing-handbook/react/ko/introduction/