
강의 목표
강의시간에 학습한 내용을 정리합니다.
강의 내용 정리
강의시간에 학습한 내용을 정리합니다.
실습 - 리액트 테스팅 라이브러리 2
- 라이브러리 기본 API 문법 익히기 (2)
- /components 디렉토리에 MyInput.tsx 파일 추가
- MyInput.tsx 에서는 nextui의 Input 그대로 export
- /components 디렉토리에 MyInput.test.tsx 파일 생성 후 테스트 작성
테스트 대상 코드
import { Input } from "@nextui-org/react";
export default function App() {
return (
<div className="flex w-full flex-wrap md:flex-nowrap gap-4">
<Input type="email" label="Email" />
<Input type="email" label="Email" placeholder="Enter your email" />
</div>
);
}
가이드 코드
import * as React from "react";
import { render, waitFor, fireEvent } from "@testing-library/react";
import MyInput from "./MyInput";
describe("MyInput", () => {
it("should render correctly", () => {
// MyInput 컴포넌트를 렌더링합니다.
// wrapper.unmount() 함수를 호출해도 에러가 발생하지 않는지 확인합니다.
});
it("should clear the value and onClear is triggered", () => {
// 필요하다면 jest mock 함수나 ref를 생성합니다.
// MyInput 컴포넌트를 렌더링합니다.
// clearButton을 클릭합니다.
// input 요소의 값이 ""인지 확인합니다.
// onClear 함수가 한 번 호출되었는지 확인합니다.
});
});
정답 코드
import * as React from "react";
import { render, waitFor, fireEvent } from "@testing-library/react";
import MyInput from "./MyInput";
describe("MyInput", () => {
it("should render correctly", () => {
// MyInput 컴포넌트를 렌더링합니다.
const wrapper = render(<MyInput label="test input" />);
// wrapper.unmount() 함수를 호출해도 에러가 발생하지 않는지 확인합니다.
expect(() => wrapper.unmount()).not.toThrow();
});
it("should clear the value and onClear is triggered", async () => {
// 필요하다면 jest mock 함수나 ref를 생성합니다.
const onClear = jest.fn();
// const ref = React.createRef<HTMLInputElement>();
// MyInput 컴포넌트를 렌더링합니다.
const { getByRole, getByDisplayValue } = render(
<MyInput
// ref={ref}
isClearable
defaultValue="junior@nextui.org"
label="test input"
onClear={onClear}
/>
);
// clearButton을 클릭합니다.
const clearButton = getByRole("button");
expect(clearButton).not.toBeNull();
fireEvent.click(clearButton);
// input 요소의 값이 ""인지 확인합니다.
// onClear 함수가 한 번 호출되었는지 확인합니다.
await waitFor(() => {
// expect(ref.current?.value)?.toBe("");
expect(getByDisplayValue("")).toBeTruthy();
expect(onClear).toHaveBeenCalledTimes(1);
});
});
});
실습 - 리액트 테스팅 라이브러리 3
- 라이브러리 기본 API 문법 익히기(3)
- /components 디렉토리에 Counter.tsx 파일 추가
- /components 디렉토리에 Counter.test.jsx 파일 생성 후 테스트 작성
- Counter.test.jsx 파일의 구조는 직접 작성
- 체크박스를 체크했을 때 document.title이 잘 변경되고, 해제했을 때 다시 기본값으로 돌아오는 것도 검증할 것
테스트 대상 코드
import React, { useState, useEffect, useRef } from "react";
const Button = (props: { onClick: () => void; text: string }) => {
return <button onClick={props.onClick}>{props.text}</button>;
};
function Counter() {
const [count, setCount] = useState(0);
const [checked, setChecked] = useState(false);
const initialTitleRef = useRef(document.title);
useEffect(() => {
document.title = checked
? `Total number of clicks: ${count}`
: initialTitleRef.current;
}, [checked, count]);
return (
<div>
<span data-testid="count">
Clicked {count} time{count === 1 ? "" : "s"}
</span>
<br />
<Button onClick={() => setCount((prev) => prev + 1)} text="Increment" />
<div>
<input
type="checkbox"
id="checkbox-title"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
/>
<label htmlFor="checkbox-title">
Check to display count in document title
</label>
</div>
</div>
);
}
export default Counter;
정답 코드
import React from "react";
import Counter from "./Counter";
import { render, fireEvent } from "@testing-library/react";
test("count starts with 0", () => {
const { getByTestId } = render(<Counter />);
expect(getByTestId("count").textContent).toBe("Clicked 0 times");
});
test("clicking on button increments counter", () => {
const { getByText, getByTestId } = render(<Counter />);
fireEvent.click(getByText("Increment"));
expect(getByTestId("count").textContent).toBe("Clicked 1 time");
fireEvent.click(getByText("Increment"));
expect(getByTestId("count").textContent).toBe("Clicked 2 times");
});
test("window title changes after every increment if checkbox is checked", () => {
global.window.document.title = "My Awesome App";
const { getByText, getByLabelText } = render(<Counter />);
// When checkbox is unchecked, incrementing has no effect
fireEvent.click(getByText("Increment"));
expect(global.window.document.title).toBe("My Awesome App");
// Check and assert the document title changes
const checkbox = getByLabelText("Check to display count in document title");
fireEvent.click(checkbox);
expect(global.window.document.title).toBe("Total number of clicks: 1");
// Works if you increment multiple times
fireEvent.click(getByText("Increment"));
expect(global.window.document.title).toBe("Total number of clicks: 2");
// Unchecking will return to the original document title
fireEvent.click(checkbox);
expect(global.window.document.title).toBe("My Awesome App");
});
리액트 테스팅 라이브러리 실습
- axios module mock 해서 테스트하기
- /components 디렉토리에 Login.tsx 파일 추가.
- /components 디렉토리에 Login.test.jsx 파일 생성 후 테스트 작성
- Login.test.jsx 파일의 구조는 직접 작성
- axios 모듈을 mock하는건 다음 링크 참조
- 네트워크 통신이 성공하는 경우와 실패하는 경우 두 경우에 대해 검증
- 네트워크 통신이 성공하는 경우도 로그인에 성공하는 경우와 실패하는 경우 검증
- 이를 위해 jest.fn().mockImplementation() 이용할 것
- localStorage 에 토큰이 저장되는 것도 검증
- 개별 테스트 종료시마다 로컬 스토리지 리셋 해줄것
테스트 대상 코드
import * as React from "react";
import axios from "axios";
interface InputElements extends HTMLFormControlsCollection {
usernameInput: HTMLInputElement;
passwordInput: HTMLInputElement;
}
interface FormElement extends HTMLFormElement {
readonly elements: InputElements;
}
function Login() {
const [state, setState] = React.useState({
resolved: false,
loading: false,
error: null,
});
function handleSubmit(event: React.FormEvent<FormElement>) {
event.preventDefault();
const { usernameInput, passwordInput } = event.currentTarget.elements;
setState({ loading: true, resolved: false, error: null });
axios
.post("/api/login", {
username: usernameInput.value,
password: passwordInput.value,
})
.then((r) => {
setState({ loading: false, resolved: true, error: null });
window.localStorage.setItem("token", r.data.token);
})
.catch((error) => {
setState({ loading: false, resolved: false, error: error.message });
});
}
return (
<div>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="usernameInput">Username</label>
<input id="usernameInput" />
</div>
<div>
<label htmlFor="passwordInput">Password</label>
<input id="passwordInput" type="password" />
</div>
<button type="submit">Submit{state.loading ? "..." : null}</button>
</form>
{state.error ? <div role="alert">{state.error}</div> : null}
{state.resolved ? (
<div role="alert">Congrats! You're signed in!</div>
) : null}
</div>
);
}
export default Login;
정답 코드
import "@testing-library/jest-dom";
import * as React from "react";
import axios from "axios";
import { render, fireEvent, screen } from "@testing-library/react";
import Login from "./Login";
jest.mock("axios");
beforeEach(() => {
window.localStorage.removeItem("token");
});
test("allows the user to login successfully", async () => {
const fakeUserResponse = { token: "fake_user_token" };
const response = { data: fakeUserResponse };
axios.post.mockResolvedValue(response);
render(<Login />);
// fill out the form
fireEvent.change(screen.getByLabelText(/username/i), {
target: { value: "chuck" },
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: "norris" },
});
fireEvent.click(screen.getByText(/submit/i));
// just like a manual tester, we'll instruct our test to wait for the alert
// to show up before continuing with our assertions.
const alert = await screen.findByRole("alert");
// .toHaveTextContent() comes from jest-dom's assertions
// otherwise you could use expect(alert.textContent).toMatch(/congrats/i)
// but jest-dom will give you better error messages which is why it's recommended
expect(alert).toHaveTextContent(/congrats/i);
expect(window.localStorage.getItem("token")).toEqual(fakeUserResponse.token);
});
test("disallows the user when username or password is incorrect", async () => {
axios.post.mockImplementation((_endpoint, body) => {
if (body.username === "chuck" && body.password === "norris") {
const fakeUserResponse = { token: "fake_user_token" };
const response = { data: fakeUserResponse };
return Promise.resolve(response);
} else {
return Promise.reject({ message: "Unauthorized", status: 401 });
}
});
render(<Login />);
// fill out the form
fireEvent.change(screen.getByLabelText(/username/i), {
target: { value: "invalid username" },
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: "norris" },
});
fireEvent.click(screen.getByText(/submit/i));
const alert = await screen.findByRole("alert");
expect(alert).toHaveTextContent(/unauthorized/i);
expect(window.localStorage.getItem("token")).toBeNull();
});
test("handles server exceptions", async () => {
const response = { message: "Internal server error", status: 500 };
axios.post.mockRejectedValue(response);
render(<Login />);
// fill out the form
fireEvent.change(screen.getByLabelText(/username/i), {
target: { value: "chuck" },
});
fireEvent.change(screen.getByLabelText(/password/i), {
target: { value: "norris" },
});
fireEvent.click(screen.getByText(/submit/i));
// wait for the error message
const alert = await screen.findByRole("alert");
expect(alert).toHaveTextContent(/internal server error/i);
expect(window.localStorage.getItem("token")).toBeNull();
});반응형
'교육 > 코드잇 스프린트 : 단기심화 5기' 카테고리의 다른 글
| [ 코드잇 스프린트 ] 교육기간 8일차 TIL (1) | 2024.11.18 |
|---|---|
| [ 코드잇 스프린트 ] 교육기간 7일차 TIL (1) | 2024.11.16 |
| [ 코드잇 스프린트 ] 교육기간 6일차 TIL (3) | 2024.11.15 |
| [ 코드잇 스프린트 ] 교육기간 5일차 TIL (1) | 2024.11.14 |
| [ 코드잇 스프린트 ] 교육기간 4일차 TIL (3) | 2024.11.13 |