[ 코드잇 스프린트 ] 교육기간 9일차 TIL

2024. 11. 21. 23:39·교육/코드잇 스프린트 : 단기심화 5기


강의 목표

강의시간에 학습한 내용을 정리합니다.

 

강의 내용 정리

강의시간에 학습한 내용을 정리합니다.

 

실습 - 리액트 테스팅 라이브러리 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&apos;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
'교육/코드잇 스프린트 : 단기심화 5기' 카테고리의 다른 글
  • [ 코드잇 스프린트 ] 교육기간 8일차 TIL
  • [ 코드잇 스프린트 ] 교육기간 7일차 TIL
  • [ 코드잇 스프린트 ] 교육기간 6일차 TIL
  • [ 코드잇 스프린트 ] 교육기간 5일차 TIL
heee1
heee1
FE 개발자를 희망하는 임희원 입니다.
  • heee1
    heee1.blog
    heee1
  • 전체
    오늘
    어제
    • 분류 전체보기 (69)
      • Front-end (1)
        • Javascript (4)
        • Typescript (1)
        • React (0)
        • Next.js (1)
        • Tool (1)
        • Git (1)
        • Prettier (0)
        • Test-Framework (1)
        • Vercel (1)
      • 교육 (28)
        • 항해99 : 웹개발 종합반 18기 (14)
        • 프로젝트 캠프 : React 2기 (5)
        • 코드잇 스프린트 : 단기심화 5기 (9)
      • Algorithm (29)
        • Javascript (24)
        • Python (5)
      • 코드 보관함 (1)
        • 배치스크립트 ( .bat ) (1)
  • 블로그 메뉴

    • 홈
  • 링크

    • github
  • 공지사항

  • 인기 글

  • 태그

    react-spring
    speed insights
    모킹
    항해99
    tailwindcss
    스프린트
    next.js
    Baekjoon
    jest
    JavaScript
    백준
    테스트 프레임워크
    오블완
    til
    코드잇
    자바스크립트
    css animation
    티스토리챌린지
    node.js
    Python
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
heee1
[ 코드잇 스프린트 ] 교육기간 9일차 TIL
상단으로

티스토리툴바