การทดสอบคอมโพเนนต์ในทางปฏิบัติ

การทดสอบคอมโพเนนต์เป็นจุดเริ่มต้นที่ดีในการสาธิตโค้ดการทดสอบที่ใช้งานได้จริง การทดสอบคอมโพเนนต์มีความสําคัญมากกว่าการทดสอบหน่วยแบบง่าย มีความซับซ้อนน้อยกว่าการทดสอบจากต้นทางถึงปลายทาง และแสดงให้เห็นถึงการโต้ตอบกับ DOM ในแง่ปรัชญา การใช้ React ช่วยให้นักพัฒนาเว็บคิดถึงเว็บไซต์หรือเว็บแอปว่าประกอบไปด้วยคอมโพเนนต์ได้ง่ายขึ้น

ดังนั้นการทดสอบคอมโพเนนต์แต่ละอย่างจะซับซ้อนเพียงใดก็ตาม จึงเป็นวิธีที่ดีในการเริ่มพิจารณาทดสอบแอปพลิเคชันใหม่หรือแอปพลิเคชันที่มีอยู่

หน้านี้จะแนะนำการทดสอบคอมโพเนนต์ขนาดเล็กที่มีการพึ่งพาภายนอกที่ซับซ้อน การทดสอบคอมโพเนนต์ที่ไม่ได้โต้ตอบกับโค้ดอื่นๆ นั้นทําได้ง่ายๆ เช่น คลิกปุ่มและยืนยันว่าตัวเลขเพิ่มขึ้น ในความเป็นจริง โค้ดแบบนั้นน้อยมาก และการทดสอบโค้ดที่ไม่มีการโต้ตอบก็อาจมีมูลค่าที่จำกัด

คอมโพเนนต์อยู่ระหว่างทดสอบ

เราใช้ Vitest และสภาพแวดล้อม JSDOM เพื่อทดสอบคอมโพเนนต์ React ซึ่งช่วยให้เราทำการทดสอบได้อย่างรวดเร็วโดยใช้ Node ในบรรทัดคำสั่งขณะจำลองเบราว์เซอร์

รายชื่อพร้อมปุ่ม "เลือก" ข้างชื่อแต่ละชื่อ
คอมโพเนนต์ React ขนาดเล็กที่แสดงรายชื่อผู้ใช้จากเครือข่าย

คอมโพเนนต์ React ชื่อ UserList นี้จะดึงข้อมูลรายชื่อผู้ใช้จากเครือข่าย และให้คุณเลือกผู้ใช้รายใดรายหนึ่งได้ ระบบจะรับรายชื่อผู้ใช้โดยใช้ fetch ภายใน useEffect และส่งตัวแฮนเดิลการเลือกโดย Context นี่คือโค้ด

import React, { useEffect, useState, useContext } from 'react';
import { UserContext } from './UserContext.tsx';
import { UserRow } from './UserRow.tsx';

export function UserList({ count = 4 }: { count?: number }) {
  const [users, setUsers] = useState<any[]>([]);
  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/users?_limit=' + count)
      .then((response) => response.json())
      .then((json) => setUsers(json));
  }, [count]);

  const c = useContext(UserContext);
  return (
    <div>
      <h2>Users</h2>
      <ul>
        {users.map((u) => (
          <li key={u.id}>
            <button onClick={() => c.userChosen(u.id)}>Choose</button>{' '}
            <UserRow u={u} />
          </li>
        ))}
      </ul>
    </div>
  );
}

ตัวอย่างนี้ไม่ได้แสดงแนวทางปฏิบัติแนะนำของ React (เช่น ใช้ fetch ภายใน useEffect) แต่โค้ดเบสของคุณมีแนวโน้มที่จะมีหลายกรณีที่คล้ายกัน ยิ่งไปกว่านั้น กรณีเหล่านี้อาจดูไม่น่าสนใจเมื่อต้องทดสอบตั้งแต่แรกเห็น ส่วนถัดไปของหลักสูตรนี้จะพูดถึงการเขียนโค้ดที่ทดสอบได้โดยละเอียด

ต่อไปนี้คือสิ่งที่เรากำลังทดสอบในตัวอย่างนี้

  • ตรวจสอบว่ามีการสร้าง DOM ที่ถูกต้องบางส่วนตามข้อมูลจากเครือข่าย
  • ยืนยันว่าการคลิกผู้ใช้ทริกเกอร์การโทรกลับ

คอมโพเนนต์แต่ละรายการจะแตกต่างกัน อะไรที่ทำให้การทดสอบนี้น่าสนใจ

  • และใช้ fetch ส่วนกลางเพื่อขอข้อมูลแบบเรียลไทม์จากเครือข่าย ซึ่งอาจทำงานไม่สม่ำเสมอหรือช้าในระหว่างการทดสอบ
  • ไฟล์ดังกล่าวนําเข้าอีกคลาสหนึ่งชื่อ UserRow ซึ่งเราอาจไม่ต้องการทดสอบโดยนัย
  • โดยใช้ Context ซึ่งไม่ได้เป็นส่วนหนึ่งของโค้ดที่ทดสอบโดยเฉพาะ และโดยปกติแล้วคอมโพเนนต์หลักจะเป็นผู้ระบุ

เขียนการทดสอบสั้นๆ เพื่อเริ่มต้น

เราทดสอบข้อมูลเบื้องต้นเกี่ยวกับคอมโพเนนต์นี้ได้อย่างรวดเร็ว ขออธิบายให้ชัดเจน ตัวอย่างนี้ ไม่ได้มีประโยชน์มากนัก แต่การตั้งค่าไฟล์ข้อมูลพร็อพเพอร์ตี้ในไฟล์ peer ที่ชื่อ UserList.test.tsx (โปรดทราบว่าโปรแกรมรันทดสอบอย่าง Vitest จะเรียกใช้ไฟล์ที่ลงท้ายด้วย .test.js หรือคล้ายกันโดยค่าเริ่มต้น ซึ่งรวมถึง .tsx) มีประโยชน์ดังนี้

import { vi, test, assert, afterAll } from 'vitest';
import { render } from '@testing-library/react';
import { UserList } from './UserList.tsx';
import React, { ContextType } from 'react';

test('render', async () => {
  const c = render(<UserList />);

  const headingNode = await c.findAllByText(/Users);
  assert.isNotNull(headingNode);
});

การทดสอบนี้ยืนยันว่าเมื่อคอมโพเนนต์แสดงผล จะมีข้อความ "ผู้ใช้" โดยจะทำงานแม้ว่าคอมโพเนนต์จะมีผลข้างเคียงจากการส่ง fetch ไปยังเครือข่ายก็ตาม fetch ยังดำเนินอยู่เมื่อสิ้นสุดการทดสอบ โดยไม่มีปลายทางที่กําหนด เราไม่สามารถยืนยันได้ว่ามีการแสดงข้อมูลผู้ใช้เมื่อการทดสอบสิ้นสุดลง อย่างน้อยก็จนกว่าจะหมดเวลา

การจำลอง fetch()

การจำลองคือการนำสิ่งที่คุณควบคุมได้มาใช้แทนฟังก์ชันหรือคลาสจริงสำหรับการทดสอบ ซึ่งเป็นแนวทางปฏิบัติทั่วไปในการทดสอบเกือบทุกประเภท ยกเว้นการทดสอบหน่วยแบบง่ายที่สุด โปรดดูข้อมูลเพิ่มเติมในการยืนยันและรูปแบบพื้นฐานอื่นๆ

คุณสามารถจำลอง fetch() มาใช้ในการทดสอบเพื่อให้เสร็จสมบูรณ์อย่างรวดเร็วและแสดงข้อมูลที่คุณคาดหวัง ไม่ใช่ข้อมูล "ในชีวิตจริง" หรือที่ไม่รู้จัก fetch เป็นตัวแปรส่วนกลาง ซึ่งหมายความว่าเราไม่จำเป็นต้อง import หรือ require ลงในโค้ด

ใน Vitest คุณจะจำลองทั่วโลกได้โดยเรียกใช้ vi.stubGlobal ด้วยออบเจ็กต์พิเศษที่แสดงผลโดย vi.fn() ซึ่งจะสร้างการจำลองที่เราแก้ไขได้ในภายหลัง เราจะอธิบายวิธีการเหล่านี้อย่างละเอียดในส่วนถัดไปของหลักสูตรนี้ แต่คุณสามารถดูวิธีการใช้งานได้ในโค้ดต่อไปนี้

test('render', async () => {
  const fetchMock = vi.fn();
  fetchMock.mockReturnValue(
    Promise.resolve({
      json: () => Promise.resolve([{ name: 'Sam', id: 'sam' }]),
    }),
  );
  vi.stubGlobal('fetch', fetchMock);

  const c = render(<UserList />);

  const headingNode = await c.queryByText(/Users);
  assert.isNotNull(headingNode);

  await waitFor(async () => {
    const samNode = await c.queryByText(/Sam);
    assert.isNotNull(samNode);
  });
});

afterAll(() => {
  vi.unstubAllGlobals();
});

โค้ดนี้จะเพิ่มการจําลอง อธิบายการดึงข้อมูลเครือข่ายเวอร์ชันจําลอง Response แล้วรอให้ปรากฏ หากข้อความไม่ปรากฏขึ้น (คุณสามารถตรวจสอบได้โดยเปลี่ยนข้อความค้นหาใน queryByText เป็นชื่อใหม่) การทดสอบจะดำเนินการไม่สำเร็จ

ตัวอย่างนี้ใช้ตัวช่วยการจำลองในตัวของ Vitest แต่เฟรมเวิร์กการทดสอบอื่นๆ ก็มีแนวทางที่คล้ายกันในการจำลอง Vitest มีความพิเศษตรงที่คุณต้องเรียกใช้ vi.unstubAllGlobals() หลังจากการทดสอบทั้งหมด หรือตั้งค่าตัวเลือกส่วนกลางที่เทียบเท่า หากไม่ "เลิกทำ" งานของเรา fetch mock อาจส่งผลต่อการทดสอบอื่นๆ และระบบจะตอบกลับคําขอทุกรายการด้วย JSON กองใหญ่

การนําเข้าจำลอง

คุณอาจสังเกตเห็นว่าคอมโพเนนต์ UserList ของเรานําเข้าคอมโพเนนต์ที่ชื่อ UserRow แม้ว่าเราจะไม่ได้ใส่โค้ดไว้ แต่คุณจะเห็นว่ามันแสดงผลชื่อผู้ใช้: การทดสอบก่อนหน้านี้จะตรวจหา "Sam" ซึ่งไม่ได้แสดงผลภายใน UserList โดยตรง จึงต้องมาจาก UserRow

โฟลว์ชาร์ตแสดงวิธีที่ผู้ใช้เลื่อนผ่านคอมโพเนนต์ของเรา
UserListTest ไม่มีสิทธิ์เข้าถึงUserRow

อย่างไรก็ตาม UserRow อาจเป็นคอมโพเนนต์ที่ซับซ้อน ซึ่งอาจดึงข้อมูลผู้ใช้เพิ่มเติม หรืออาจมีผลข้างเคียงที่ไม่เกี่ยวข้องกับการทดสอบของเรา การนําความแปรปรวนนั้นออกทําให้การทดสอบมีประโยชน์มากขึ้น โดยเฉพาะเมื่อคอมโพเนนต์ที่คุณต้องการทดสอบมีความซับซ้อนมากขึ้นและเชื่อมโยงกับข้อกําหนดของคอมโพเนนต์มากขึ้น

แต่คุณใช้ Vitest เพื่อจำลองการนําเข้าบางอย่างได้ แม้ว่าการทดสอบจะไม่ใช้การนําเข้านั้นโดยตรงก็ตาม เพื่อให้โค้ดที่ใช้การนําเข้ามีเวอร์ชันที่เรียบง่ายหรือเป็นที่รู้จัก ดังนี้

vi.mock('./UserRow.tsx', () => {
  return {
    UserRow(arg) {
      return <>{arg.u.name}</>;
    },
  }
});

test('render', async () => {
  // ...
});

เช่นเดียวกับการจำลอง fetch ระดับส่วนกลาง นี่เป็นเครื่องมือที่มีประสิทธิภาพ แต่อาจไม่ยั่งยืนหากโค้ดของคุณมี Dependency จำนวนมาก อีกครั้ง การแก้ไขที่ดีที่สุดคือการเขียนโค้ดที่ทดสอบได้

คลิกและระบุบริบท

React และไลบรารีอื่นๆ เช่น Lit มีแนวคิดที่เรียกว่า Context โค้ดตัวอย่างมี UserContext ซึ่งจะเรียกใช้เมธอดหากเลือกผู้ใช้ มักใช้เป็นทางเลือกแทน "การเจาะพร็อพเพอร์ตี้" ซึ่งระบบจะส่งการเรียกกลับไปยัง UserList โดยตรง

โปรแกรมทดสอบอัตโนมัติของเราไม่ได้ระบุ UserContext การเพิ่มการคลิกไปที่การทดสอบ React โดยไม่มีการดำเนินการนี้อาจทำให้การทดสอบขัดข้องได้ ในกรณีที่ดี หากมีการสร้างอินสแตนซ์เริ่มต้นไว้ที่อื่น อาจทําให้ลักษณะการทํางานบางอย่างอยู่นอกเหนือการควบคุมของเรา (คล้ายกับ UserRow ที่ไม่รู้จักด้านบน)

  const c = render(<UserList />);
  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();

คุณจะระบุ Context ของคุณเองได้เมื่อแสดงผลคอมโพเนนต์ ตัวอย่างนี้ใช้อินสแตนซ์ของ vi.fn() ซึ่งเป็นฟังก์ชัน Vitest Mock ที่ใช้ตรวจสอบได้ว่ามีการเรียกหรือไม่และใช้อาร์กิวเมนต์ใด

ในกรณีของเรา การดำเนินการนี้จะโต้ตอบกับ fetch ที่จำลองในตัวอย่างก่อนหน้านี้ และการทดสอบสามารถยืนยันว่ารหัสที่ส่งผ่านคือ sam

  const userChosenFn = vi.fn();
  const ucForTest: ContextType<typeof UserContext> = { userChosen: userChosenFn as any };
  const c = render(
    <UserContext.Provider value={ucForTest}>
      <UserList />
    </UserContext.Provider>,
  );

  const chooseButton = await c.getByText(/Choose);
  chooseButton.click();
  assert.deepStrictEqual(userChosenFn.mock.calls, [['sam']]);

นี่เป็นรูปแบบที่เรียบง่ายแต่มีประสิทธิภาพซึ่งช่วยให้คุณนำข้อกำหนดที่ไม่เกี่ยวข้องออกจากคอมโพเนนต์หลักที่พยายามทดสอบได้

ข้อมูลสรุป

ตัวอย่างนี้แสดงวิธีสร้างการทดสอบคอมโพเนนต์เพื่อทดสอบและปกป้องคอมโพเนนต์ React ที่ทดสอบได้ยาก การทดสอบนี้มุ่งเน้นที่การตรวจสอบว่าคอมโพเนนต์โต้ตอบกับทรัพยากร Dependency อย่างถูกต้อง ซึ่งได้แก่ fetch ส่วนกลาง คอมโพเนนต์ย่อยที่นำเข้า และ Context

ทดสอบความเข้าใจ

วิธีใดที่ใช้ในการทดสอบคอมโพเนนต์รีแอ็กชัน

ทั่วโลกการสกัด
ตรวจสอบว่าตัวเลขที่เพิ่มขึ้น
จำลองทรัพยากร Dependency ที่ซับซ้อนด้วยทรัพยากรแบบง่ายสำหรับการทดสอบ
การฉีด Dependency โดยใช้ Context