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

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

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

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

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

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

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

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

คอมโพเนนต์ React ชื่อ UserList นี้จะดึงรายชื่อผู้ใช้จากเครือข่ายและให้คุณเลือก 1 รายการ ระบบจะรับรายชื่อผู้ใช้โดยใช้ 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 ซึ่งไม่ได้เป็นส่วนหนึ่งของโค้ดที่อยู่ระหว่างการทดสอบโดยเฉพาะ และโดยปกติจะได้จากคอมโพเนนต์ระดับบนสุด

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

เราสามารถทดสอบสิ่งที่พื้นฐานมากๆ เกี่ยวกับคอมโพเนนต์นี้ได้อย่างรวดเร็ว เพื่อความชัดเจน ตัวอย่างนี้ ไม่มีประโยชน์มากนัก แต่การตั้งค่าต้นแบบในไฟล์เพียร์ที่ชื่อว่า 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()

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

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

ในการทดสอบ คุณอาจจำลองเหตุการณ์ทั่วโลกได้โดยเรียกใช้ 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 อาจส่งผลต่อการทดสอบอื่นๆ และคำขอทั้งหมดจะได้รับการตอบกลับด้วยกอง JSON ที่แปลกใหม่ของเรา!

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

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

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

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

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

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

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

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

คลิกและให้บริบท

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

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

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

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

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

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

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