Today Sangmin Learned
728x90

어제 포스팅에 이어서 여러 조건에 대한 테스팅을 추가로 포스팅해보려고 한다.

볼 것은 상태 변경 시 바뀌는 렌더링 값에 대한 테스팅, HTTP 요청 전송 시 렌더링 여부에 대한 테스팅이다.

1. src/components/Greeting.js

import { useState } from "react";

const Greeting = () => {
  const [changedText, setChangedText] = useState(false);

  const changeTextHandler = () => {
    setChangedText(true);
  };

  return (
    <div>
      <h2>Hello World!</h2>
      {!changedText && <p>만나서 반가워요!</p>}
      {changedText && <p>이렇게 바뀌었어요!</p>}
      <button onClick={changeTextHandler}>Change Text!</button>
    </div>
  );
};

export default Greeting;

Change Text! 버튼을 누르면 만나서 반가워요!에서 이렇게 바뀌었어요!로 바뀌는 간단한 컴포넌트이다.

2. src/components/Greeting.test.js

2-1) 버튼을 클릭하지 않았을 때 '만나서 반가워요!'가 렌더링되는지 검사

import { render, screen } from "@testing-library/react";
import Greeting from "./Greeting";

describe("Greeting 컴포넌트", () => {
  test("버튼을 클릭하지 않았을 때 '만나서 반가워요!'가 렌더링 되는지 검사해볼게요", () => {
    render(<Greeting />);

    const paragraphElement = screen.getByText("만나서 반가워요", {
      exact: false,
    });
    expect(paragraphElement).toBeInTheDocument();
  });
};

이 테스팅 코드는 그냥 값이 있는지 없는지를 검사하는 코드이다. getByText를 사용한다면 무조건 렌더링하는 컴포넌트 내에 해당 값이 있어야지만 테스트를 통과한다. changedText의 기본 상태가 false이므로 "만나서 반가워요"가 렌더링 되어있을 것이다. 따라서 이 테스트는 통과한다.

2-2) 버튼을 클릭했을 때 '이렇게 바뀌었어요!'가 렌더링되는지 검사

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Greeting from "./Greeting";

describe("Greeting 컴포넌트", () => {
   test("버튼을 클릭했을 때 '이렇게 바뀌었어요!'가 렌더링 되는지 검사해볼게요", () => {
    // 1. Arrange
    render(<Greeting />);

    // 2. Act
    const buttonElement = screen.getByRole("button");
    userEvent.click(buttonElement);

    // 3. Assert
    const changedTextElement = screen.getByText("이렇게 바뀌었어요", {
      exact: false,
    });
    expect(changedTextElement).toBeInTheDocument();
  });
};

우선 buttonElement라는 변수에 screen.getByRole()을 통해 특정 엘리먼트를 받아와서 선언할 수 있다. 그 다음 버튼을 클릭했다라는 조건이 필요하므로 userEvent가 들어간다. 결국 2번의 Act는 버튼 엘리먼트를 불러와서 그것을 누르는 상황을 가정한 것이다.

3번의 Assert는 위에서 설명한 대로 getByText로 "이렇게 바뀌었어요"를, toBeInTheDocument()로 렌더링 한 컴포넌트 내에 값이 있는지 확인한다.

2-3) 버튼을 클릭했을 때 '만나서 반가워요!'가 사라지는지 검사

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Greeting from "./Greeting";

describe("Greeting Component", () => {
  test("버튼을 클릭했을 때 '만나서 반가워요!'가 사라지는지 검사해볼게요", () => {
    // 1. Arrange
    render(<Greeting />);

    // 2. Act
    const buttonElement = screen.getByRole("button");
    userEvent.click(buttonElement);

    // 3. Assert
    const disappearedTextElement = screen.queryByText("만나서 반가워요", {
      exact: false,
    });
    expect(disappearedTextElement).toBeNull();
  });
});

마찬가지로 버튼 엘리먼트를 상정하고 클릭하는 이벤트를 Act에 부여했는데, 여기서는 getByText가 아니라 queryByText를 사용한다. 상술한 대로 getByText는 무조건 테스팅 시점에 해당 값이 렌더링 되어있는지 아닌지를 검사하기 때문에 사라졌는지의 여부는 테스팅할 수 없다. 따라서 사라지는 요소에 대해서는 queryByText라는 대체재를 사용하며, 마지막에 무엇을 테스트하는지에 대한 expect 구문에서는 .toBeNull() 메소드를 통해 해당 값이 렌더링 시점에서 null을 갖는지 여부를 검사한다.

3. src/components/Async.js

다음으로 간단한 API통신 컴포넌트를 살펴보겠다. 여기서 테스팅 할 것은 API통신의 결과값이 제대로 렌더링되는지의 여부이다.

import { useEffect, useState } from "react";

const Async = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then((response) => response.json())
      .then((data) => {
        setPosts(data);
      });
  }, []);

  return (
    <div>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Async;

Mock Data API를 다루는 jsonplaceholder에서 API통신을 하여 성공한 경우 setPosts를 통해 posts상태에 결과값을 넣고, return 문에서 map을 통해 그 값을 뿌려주는 형태의 간단한 컴포넌트이다.

4. src/components/Async.test.js

import { render, screen } from "@testing-library/react";
import Async from "./Async";

describe("Async 컴포넌트", () => {
  test("요청 성공 시 글이 렌더링 되는지 검사해볼게요", async () => {
  window.fetch = jest.fn();
  window.fetch.mockResolvedValueOnce({
    json: async () => [{ id: "1", title: "첫번째 글!" }],
  });
  render (<Async />);

  const listItemElements = await screen.findAllByRole("listItem", {
    exact: false,
  });
  expect(listItemElements).not.toHaveLength(0);
  });
};

API 통신에 대한 테스팅의 경우, 테스트를 진행할 때 실제로 HTTP 요청을 보내지는 않는다. 지금이야 API 통신을 하는 컴포넌트가 이거 하나 뿐이라 실제로 보내더라도 상관없겠지만 앱의 규모가 커진다면 전체 테스팅을 할 경우 API 통신이 한꺼번에 이뤄져 트래픽이 증가하여 서버에 과부하를 줄 수 있기 때문이기도 하고, 무엇보다 특히 POST 요청을 보낸다면 테스팅 과정에서 DB에 응답값이 저장될 것이기 때문이다.

그래서 이 경우는 보통 실제로 요청을 전송하지 않거나, 일종의 테스팅 서버로 요청을 전송하는 방식을 사용한다. 여기서는 실제로 요청을 전송하지 않는 방법을 사용한다. 즉 더미 함수를 만들어 대신 호출하는 것이다.

window.fetch = jest.fn();
window.fetch.mockResolvedValueOnce({
  json: async () => [{ id: "1", title: "첫번째 글!" }],
});

Jest에서 fn()을 통해 더미 함수를 만든다. 그다음 mockResolvedValueOnce라는 메소드를 사용하는데, 이는 fetch 함수가 호출되었을 때 결정되어야 하는 값을 설정할 수 있게 해준다. 무슨 뜻이냐면, 새롭게 만든 더미함수에 대해 API통신이 이뤄졌을 때 반환하고 싶은 값을 정의하는 것이다.

내부의 객체를 보면, key값이 json으로 되어있는데, 이는 JSON이 호출되었을 때 반환하고 싶은 것을 value로 설정할 수 있다는 것이다.

임의의 데이터인 { id: "1", title: "첫번째 글!" }가 들어있는 JSON 객체를 만드는 것이다.

이러한 일련의 과정은, 내장 fetch 함수를 더미 fetch 함수로 덮어쓰고 있는 것이다. 이를 통해 불필요한 API통신이 사라지게 된다. Jest에서 더미 함수를 만들어 대신 반환할 값까지 우리가 직접 지정할 수 있다는 사실이 참 놀라운 것 같다.

  const listItemElements = await screen.findAllByRole("listItem", {
    exact: false,
  });
  expect(listItemElements).not.toHaveLength(0);

그다음 li 엘리먼트를 처리해야 하는데, 여기서 API 통신의 결과값이 두 개 이상 나타난다. Async.js에서 fetch의 대상으로 설정한 URI를 보면 값이 여러개다. 이와 같이 두 개 이상의 아이템이 예상될 경우 getAllByRole을 사용해야 한다. 그렇지만 getAllByRole을 사용할 경우 테스팅에 실패하게 되는데, 이는 HTTP 요청(비동기)을 가정하지 않았기 때문이다. 즉 HTTP 요청이 성공적으로 이뤄지기 전에는 li에 값이 없는 상태이기 때문에 에러가 생기는 것이다.

이 경우 findAllByRole 메소드를 사용하지만 이는 비동기를 전제로 하기 때문에 async / await 선언이 되어있어야 한다. 이러한 이유로 프로미스 객체를 반환하는 async / await이 사용된 것이다.

마지막으로 listItemElements의 길이가 0인지 아닌지를 검사하는 것으로 테스팅이 완료된다.

profile

Today Sangmin Learned

@steadily-worked

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!