TDD가 무엇이고 왜 필요한지는 훌륭한 글들이 많기에... 내가 학습한 내용들의 일부를 정리하고자 한다.
udemy 강의를 듣고 정리
React Testing Library builds on top of DOM Testing Library by adding APIs for working with React components.
Jest is a delightful JavaScript Testing Framework with a focus on simplicity
RTL은 Virtual DOM을 제공한다. 또한 DOM과 상호 작용할 수 있는 유틸리티 또한 제공한다. 어떤 요소를 click하거나 하는것들 말이다.
RTL은 내부 코드를 테스트 하는 것보다 사용자의 사용에 초점이 맞춰져있다. 사용자가 앱을 사용하는 것을 테스트한다.
Unit 테스트는 사용자가 앱과 상호작용하는 것과는 좀 거리가 있는 테스팅이다. 또한 리팩토링으로 동작에 변화는 없지만 코드가 변했다면 실패할 수 있다. 반면 테스트를 실패한 지점이 명확하게 드러난다.
Unit 테스트와는 달리 사용자가 앱을 사용하는 플로우와 연관되어있다. 코드가 변해도 동작에 변화가 없는 리팩토링 시에도 문제 없이 테스팅 된다. 그렇지만 테스트 디버깅에 어려움이 있을 수 있다.
간단하게 CRA template typescript로 연습용 만들어서 가보즈아.
첫번째 테스트 코드는 기존에 있던 코드다.
두번째 테스트 코드가 통과하는 이유는 a 태그가 기본적으로 role이 link이기 때문이다.
test("renders learn react link", () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
test("App component have link", () => {
render(<App />);
const linkElement = screen.getByRole("link", { name: /learn react/i });
expect(linkElement).toBeInTheDocument();
});
그냥 함수 테스트도 해볼까?
const removeApple = (arr: string[]) => {
arr.pop();
};
test("사과가 사라졌어요!", () => {
const arr = ["🍕", "🌭", "🥓", "🍎"];
removeApple(arr);
expect(arr).toHaveLength(2);
expect(arr).toEqual(["🍕", "🌭"]);
});
// Expected length: 2
// Received length: 3
// Received array: ["🍕", "🌭", "🥓"]
위의 테스트 코드는 틀렸다고 알려주고 있다. 그리고 어떤 결과가 나왔는지까지 알려주고 있다.
test("사과가 사라졌어요!", () => {
const arr = ["🍕", "🌭", "🥓", "🍎"];
removeApple(arr);
expect(arr).toHaveLength(3);
expect(arr).toEqual(["🍕", "🌭", "🥓"]);
});
이제 통과된다.
This helper function can be used to print out a list of all the implicit ARIA roles within a tree of DOM nodes, each role containing a list of all of the nodes which match that role. This can be helpful for finding ways to query the DOM under test with getByRole.
이 함수로 ARIA 역할을 볼 수 있다.
// Button/index.tsx
function Button() {
return <button style={{ backgroundColor: "red" }}>Click!</button>;
}
// Button/index.test.tsx
import React from "react";
import { render, screen } from "@testing-library/react";
import { logRoles } from "@testing-library/react";
import Button from ".";
test("button has red color", () => {
const { container } = render(<Button />);
logRoles(container);
const buttonElement = screen.getByRole("button", { name: /click!/i });
expect(buttonElement).toHaveStyle("backgroundColor: red");
});
user-event 사용을 더 권고하고 있는듯? 하다.
아무튼 사용해보면
test("button turns blue when clicked", () => {
render(<Button />);
const colorButton = screen.getByRole("button", { name: /click!/i });
expect(colorButton).toHaveStyle("backgroundColor: red");
fireEvent.click(colorButton);
expect(colorButton).toHaveStyle("backgroundColor: blue");
});
버튼을 찾아서 click하면 backgroundColor를 바꿔주는 테스트다.
여러개의 체크박스가 있고 나는 하나를 특정해서 골라내고 싶다면 이때 label을 사용할 수 있다.
// Button.tsx
<label htmlFor="target-checkbox">Disable Button</label>
<input
type="checkbox"
id="target-checkbox"
/>
<input type="checkbox" />
<input type="checkbox" />
// test.tsx
test("check button enabled when click checkbox", () => {
render(<Button />);
const checkbox = screen.getByRole("checkbox", { name: /disable button/i });
expect(checkbox).toBeInTheDocument();
});
해당 플러그인에 대해 알아보면 좋다.
eslint-plugin-testing-library
eslint-plugin-jest-dom
.eslintcache는 gitignore에 추가해주자
udemy강의를 듣는데 이제 스스로 코딩을 하고 다음에 해답을 보게 된다. 해답이라기 보다는 강사님은 자기는 이런식으로 했다가 더 맞는 표현이겠다.
간략하게 비교해보면
// test 코드 먼저 짠다.
test("SummaryForm initial setting", () => {
render(<SummaryForm />);
const checkbox = screen.getByRole("checkbox", { name: /i agree to/i });
expect(checkbox).toBeInTheDocument();
});
위 코드는 당연히 실패한다. 리액트 코드가 없으니까.
그리고 테스트에 맞게 내가 코드를 작성해 넣는다.
function SummaryForm() {
return (
<div>
<label htmlFor="agree-checkbox">I agree to Terms and Conditions</label>
<input
type="checkbox"
id="agree-checkbox"
onChange={handleChangeCheckbox}
/>
</div>
);
}
export default SummaryForm;
이제 checkbox가 클릭 되었으면 버튼을 활성화 시키고, 클릭이 되어있지 않다면 버튼을 비활성화 시킬 것이다.
userEvent의 경우 14버전으로 업그레이드 해서 사용했다. 14버전의 경우 모든 api가 promise반환하므로 async, await을 사용해야한다.
user-event 14
// test
test("disalbed button when click checkbox", async () => {
const user = userEvent.setup();
render(<SummaryForm />);
const checkbox = screen.getByRole("checkbox", { name: /i agree to/i });
const button = screen.getByRole("button", { name: /confirm order/i });
await user.click(checkbox);
expect(button).toBeEnabled();
await user.click(checkbox);
expect(button).toBeDisabled();
});
당연히 실패하겠지? 버튼도 아직 안 만들었고, 상태도 없다.
function SummaryForm() {
const [isChecked, setIsChecked] = useState(false);
const handleChangeCheckbox = (e: React.ChangeEvent<HTMLInputElement>) => {
setIsChecked(e.target.checked);
};
return (
<div>
<label htmlFor="agree-checkbox">I agree to Terms and Conditions</label>
<input
type="checkbox"
id="agree-checkbox"
onChange={handleChangeCheckbox}
/>
<button disabled={!isChecked}>click</button>
</div>
);
}
export default SummaryForm;
테스트는 통과했고, 강좌는 react bootstrap을 사용하기에 그에 맞게 react 코드를 바꿨다. 그래도 역시 테스트는 통과했다.
내가 작성한 부분에 스타일링 테스트는 없었기 때문이겠지.
강사님과 다른점은 초기세팅 부분에서 최초에 체크박스가 선택되어 있지 않다는 점까지도 테스트를 하셨다. 그 외에는 거의 동일하다.
사실 테스트란게 어디부터 어디까지 해야하는지 아직 감을 잡지 못했다. 그렇기에 연습하고 있는거고.
앞으로 계속 퀴즈 형식으로 나오는 테스트 코드를 직접 쳐 보려고 한다. 나름 재밌을지도..?
이후 테스트 부분도 글을 적을지 아니면 그냥 깃허브에 올릴지는 아직 모르겠다. 일단 열심히 들어야지.