× Giới thiệu Lịch khai giảng Tin tức Sản phẩm học viên

Khám phá React Testing Recipes từ A-Z

24/04/2023 01:25

Khám phá React Testing Recipes từ A-Z với các thành phần như Setup/Teardown, act(), Rendering Data Fetching Mocking Modules, Events Timers Snapshot Testing Multiple Renderers

Thiết lập/Chia nhỏ

Đối với mỗi thử nghiệm, chúng tôi thường muốn hiển thị cây React của mình thành phần tử DOM được đính kèm với document. Điều này rất quan trọng để nó có thể nhận các sự kiện DOM. Khi thử nghiệm kết thúc, chúng tôi muốn "dọn dẹp" và ngắt kết nối cây khỏi tệp document.

Một cách phổ biến để làm điều đó là sử dụng một cặp beforeEachvà afterEachkhối để chúng luôn chạy và cô lập các tác động của thử nghiệm đối với chính nó:

import { unmountComponentAtNode } from "react-dom";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

Bạn có thể sử dụng một mẫu khác, nhưng hãy nhớ rằng chúng tôi muốn thực hiện việc dọn dẹp ngay cả khi thử nghiệm không thành công . Nếu không, các bài kiểm tra có thể bị “rò rỉ” và một bài kiểm tra có thể thay đổi hành vi của một bài kiểm tra khác. Điều đó làm cho chúng khó gỡ lỗi.


act()

Khi viết các bài kiểm tra giao diện người dùng, các tác vụ như kết xuất, sự kiện của người dùng hoặc tìm nạp dữ liệu có thể được coi là "đơn vị" tương tác với giao diện người dùng. react-dom/test-utilscung cấp một trình trợ giúp có tên act()để đảm bảo tất cả các cập nhật liên quan đến các “đơn vị” này đã được xử lý và áp dụng cho DOM trước khi bạn thực hiện bất kỳ xác nhận nào:

act(() => {
  // render components
});
// make assertions

Điều này giúp làm cho các thử nghiệm của bạn chạy gần hơn với những gì người dùng thực sẽ trải nghiệm khi sử dụng ứng dụng của bạn. Phần còn lại của những ví dụ này sử dụng act()để thực hiện những đảm bảo này.

Bạn có thể thấy sử dụng act()trực tiếp hơi quá dài dòng. Để tránh một số bản soạn sẵn, bạn có thể sử dụng một thư viện như Thư viện kiểm tra phản ứng , có các trình trợ giúp được bao bọc bởi các tệp act().

Ghi chú:

Tên actxuất phát từ mẫu Sắp xếp-Hành động-Khẳng định .


Rendering

Thông thường, bạn có thể muốn kiểm tra xem một thành phần có hiển thị chính xác cho các đạo cụ đã cho hay không. Hãy xem xét một thành phần đơn giản hiển thị thông báo dựa trên chỗ dựa:

// 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>;
  }
}

Chúng ta có thể viết một bài kiểm tra cho thành phần này:

// 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(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("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!");
});

Data Fetching

Thay vì gọi các API thực trong tất cả các thử nghiệm của mình, bạn có thể giả định các yêu cầu bằng dữ liệu giả. Việc mô phỏng tìm nạp dữ liệu bằng dữ liệu “giả mạo” ngăn chặn các thử nghiệm không ổn định do phần phụ trợ không khả dụng và giúp chúng chạy nhanh hơn. Lưu ý: bạn vẫn có thể muốn chạy một tập hợp con các thử nghiệm bằng cách sử dụng khung “từ đầu đến cuối” để cho biết liệu toàn bộ ứng dụng có hoạt động cùng nhau hay không.

// user.js

import React, { useState, useEffect } from "react";

export default function User(props) {
  const [user, setUser] = useState(null);

  async function fetchUserData(id) {
    const response = await fetch("/" + id);
    setUser(await response.json());
  }

  useEffect(() => {
    fetchUserData(props.id);
  }, [props.id]);

  if (!user) {
    return "loading...";
  }

  return (
    <details>
      <summary>{user.name}</summary>
      <strong>{user.age}</strong> years old
      <br />
      lives in {user.address}
    </details>
  );
}

Chúng ta có thể viết các bài kiểm tra cho nó:

// 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;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("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)    })  );
  // Use the asynchronous version of act to apply resolved promises
  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);

  // remove the mock to ensure tests are completely isolated  global.fetch.mockRestore();});

Mocking modules

Một số mô-đun có thể không hoạt động tốt trong môi trường thử nghiệm hoặc có thể không cần thiết cho bản thân thử nghiệm. Việc mô phỏng các mô-đun này bằng các mô-đun thay thế giả có thể giúp bạn viết bài kiểm tra cho mã của riêng mình dễ dàng hơn.

Hãy xem xét một Contactthành phần nhúng thành phần của bên thứ ba GoogleMap:

// map.js

import React from "react";

import { LoadScript, GoogleMap } from "react-google-maps";
export default function Map(props) {
  return (
    <LoadScript id="script-loader" googleMapsApiKey="YOUR_API_KEY">
      <GoogleMap id="example-map" center={props.center} />
    </LoadScript>
  );
}

// contact.js

import React from "react";
import Map from "./map";

export default function Contact(props) {
  return (
    <div>
      <address>
        Contact {props.name} via{" "}
        <a data-testid="email" href={"mailto:" + props.email}>
          email
        </a>
        or on their <a data-testid="site" href={props.site}>
          website
        </a>.
      </address>
      <Map center={props.center} />
    </div>
  );
}

Nếu chúng tôi không muốn tải thành phần này trong các thử nghiệm của mình, chúng tôi có thể giả định chính sự phụ thuộc đó vào một thành phần giả và chạy thử nghiệm của mình:

// 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>    );  };});
let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("should render contact information", () => {
  const center = { lat: 0, long: 0 };
  act(() => {
    render(
      <Contact
        name="Joni Baez"
        email="test@example.com"
        site="http://test.com"
        center={center}
      />,
      container
    );
  });

  expect(
    container.querySelector("[data-testid='email']").getAttribute("href")
  ).toEqual("mailto:test@example.com");

  expect(
    container.querySelector('[data-testid="site"]').getAttribute("href")
  ).toEqual("http://test.com");

  expect(container.querySelector('[data-testid="map"]').textContent).toEqual(
    "0:0"
  );
});

Event

Chúng tôi khuyên bạn nên gửi các sự kiện DOM thực trên các phần tử DOM, sau đó xác nhận kết quả. Hãy xem xét một Togglethành phần:

// toggle.js

import React, { useState } from "react";

export default function Toggle(props) {
  const [state, setState] = useState(false);
  return (
    <button
      onClick={() => {
        setState(previousState => !previousState);
        props.onChange(!state);
      }}
      data-testid="toggle"
    >
      {state === true ? "Turn off" : "Turn on"}
    </button>
  );
}

Chúng ta có thể viết các bài kiểm tra cho nó:

// 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(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  document.body.appendChild(container);});
afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("changes value when clicked", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });

  // get a hold of the button element, and trigger some clicks on it
  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");
});

Các sự kiện DOM khác nhau và thuộc tính của chúng được mô tả trong MDN . Lưu ý rằng bạn cần chuyển { bubbles: true }từng sự kiện mà bạn tạo để nó đến được trình lắng nghe React vì React tự động ủy quyền các sự kiện cho thư mục gốc.

Ghi chú:

Thư viện kiểm tra phản ứng cung cấp một trình trợ giúp ngắn gọn hơn để kích hoạt các sự kiện.


Timers

Mã của bạn có thể sử dụng các chức năng dựa trên bộ đếm thời gian như setTimeoutlên lịch cho nhiều công việc hơn trong tương lai. Trong ví dụ này, bảng trắc nghiệm chờ lựa chọn và tiến lên, hết thời gian chờ nếu lựa chọn không được thực hiện trong 5 giây:

// card.js

import React, { useEffect } from "react";

export default function Card(props) {
  useEffect(() => {
    const timeoutID = setTimeout(() => {
      props.onSelect(null);
    }, 5000);
    return () => {
      clearTimeout(timeoutID);
    };
  }, [props.onSelect]);

  return [1, 2, 3, 4].map(choice => (
    <button
      key={choice}
      data-testid={choice}
      onClick={() => props.onSelect(choice)}
    >
      {choice}
    </button>
  ));
}

Chúng ta có thể viết các bài kiểm tra cho thành phần này bằng cách tận dụng bộ đếm thời gian giả của Jest và kiểm tra các trạng thái khác nhau mà nó có thể ở.

// card.test.js import React from "react"; import { render, unmountComponentAtNode } from "react-dom"; import { act } from "react-dom/test-utils"; import Card from "./card"; let container = null; beforeEach(() => { // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); jest.useFakeTimers(); }); afterEach(() => { // cleanup on exiting unmountComponentAtNode(container); container.remove(); container = null; jest.useRealTimers(); }); it("should select null after timing out", () => { const onSelect = jest.fn(); act(() => { render(<Card onSelect={onSelect} />, container); }); // move ahead in time by 100ms act(() => { jest.advanceTimersByTime(100); }); expect(onSelect).not.toHaveBeenCalled(); // and then move ahead by 5 seconds act(() => { jest.advanceTimersByTime(5000); }); expect(onSelect).toHaveBeenCalledWith(null); }); it("should cleanup on being removed", () => { const onSelect = jest.fn(); act(() => { render(<Card onSelect={onSelect} />, container); }); act(() => { jest.advanceTimersByTime(100); }); expect(onSelect).not.toHaveBeenCalled(); // unmount the app act(() => { render(null, container); }); act(() => { jest.advanceTimersByTime(5000); }); expect(onSelect).not.toHaveBeenCalled(); }); it("should accept selections", () => { const onSelect = jest.fn(); act(() => { render(<Card onSelect={onSelect} />, container); }); act(() => { container .querySelector("[data-testid='2']") .dispatchEvent(new MouseEvent("click", { bubbles: true })); }); expect(onSelect).toHaveBeenCalledWith(2); });

Bạn chỉ có thể sử dụng bộ hẹn giờ giả trong một số thử nghiệm. Ở trên, chúng tôi đã kích hoạt chúng bằng cách gọi jest.useFakeTimers(). Ưu điểm chính mà chúng mang lại là thử nghiệm của bạn không thực sự phải đợi năm giây để thực thi và bạn cũng không cần phải làm cho mã thành phần phức tạp hơn chỉ để thử nghiệm.


Snapshot Testing

Các khung như Jest cũng cho phép bạn lưu “ảnh chụp nhanh” dữ liệu với toMatchSnapshot/toMatchInlineSnapshot . Với những thứ này, chúng tôi có thể “lưu” đầu ra thành phần được kết xuất và đảm bảo rằng một thay đổi đối với nó phải được cam kết rõ ràng như một thay đổi đối với ảnh chụp nhanh.

Trong ví dụ này, chúng tôi kết xuất một thành phần và định dạng HTML được kết xuất với prettygói, trước khi lưu nó dưới dạng ảnh chụp nhanh nội tuyến:

// 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"; let container = null; beforeEach(() => { // setup a DOM element as a render target container = document.createElement("div"); document.body.appendChild(container); }); afterEach(() => { // cleanup on exiting unmountComponentAtNode(container); container.remove(); container = null; }); it("should render a greeting", () => { act(() => { render(<Hello />, container); }); expect( pretty(container.innerHTML) ).toMatchInlineSnapshot(); /* ... gets filled automatically by jest ... */ act(() => { render(<Hello name="Jenny" />, container); }); expect( pretty(container.innerHTML) ).toMatchInlineSnapshot(); /* ... gets filled automatically by jest ... */ act(() => { render(<Hello name="Margaret" />, container); }); expect( pretty(container.innerHTML) ).toMatchInlineSnapshot(); /* ... gets filled automatically by jest ... */ });

Thông thường, tốt hơn là đưa ra các xác nhận cụ thể hơn là sử dụng ảnh chụp nhanh. Các loại thử nghiệm này bao gồm các chi tiết triển khai để chúng dễ dàng bị hỏng và các nhóm có thể không nhạy cảm với các lỗi ảnh chụp nhanh. Chế nhạo có chọn lọc một số thành phần con có thể giúp giảm kích thước của ảnh chụp nhanh và giữ cho chúng có thể đọc được để xem xét mã.


Multiple Renderers

Trong một số ít trường hợp, bạn có thể đang chạy thử nghiệm trên một thành phần sử dụng nhiều trình kết xuất. Ví dụ: bạn có thể đang chạy kiểm tra ảnh chụp nhanh trên một thành phần có react-test-renderer, thành phần này sử dụng nội bộ rendertừ react-dombên trong một thành phần con để hiển thị một số nội dung. Trong trường hợp này, bạn có thể bọc các bản cập nhật bằng act()s tương ứng với trình kết xuất của chúng.

import { act as domAct } from "react-dom/test-utils";
import { act as testAct, create } from "react-test-renderer";
// ...
let root;
domAct(() => {
  testAct(() => {
    root = create(<App />);
  });
});
expect(root).toMatchSnapshot();