เวลาเราเขียนโค๊ดใน React Component มักมีการสร้างฟังก์ชันต่างๆ ภายใน component นั่นๆ เช่น การใช้งาน useState, useEffect และฟังก์ชันอื่นๆ หากเราต้องการจัดการทดสอบ component แล้วจำเป็นต้อง mock หรือ stub ฟังก์ชันภายใน component จะไม่สามารถทำได้

สมมติว่าเรามีไฟล์ component ชื่อว่า UserLists ที่แสดงรายการชื่อ user ทั้งหมด และมีการเพิ่มชื่อใหม่ (Eve) เข้าไปหลังจาก render ตามโค๊ดตัวอย่าง

import { useEffect, useState } from 'react';

const UserLists = () => {
  const [users, setUsers] = useState(['Alice', 'Bob', 'Charlie', 'David']);

  useEffect(() => {
    const newUser = users;
    newUser.push('Eve');
    setUsers(newUser);
  }, []);

  return (
    <div>
      {users.map((user, i) => (
        <p key={i}>{user}</p>
      ))}
    </div>
  );
};

export default UserLists;

UserLists.tsx

จากโค๊ดจะเห็นว่าเป็นวิธีการเขียนที่เราจะเจอได้ทั่วไป ซึ่งโค๊ดของ component นี้ เราจะไม่สามารถทดสอบฟังก์ชันภายได้ใน เนื่องจาก Cypress ไม่สามารถเข้าถึงมันได้

Solution

วิธีปรับปรุงโค๊ดเพื่อให้ง่ายต่อการทำการทดสอบคือ จะต้องแยกการทำงานของส่วนฟังก์ชันภายใน component นี้ ออกไปเป็นฟังก์ชัน hook เสียก่อน

import { useEffect, useState } from 'react';

function useUser() {
  const [users, setUsers] = useState(['Alice', 'Bob', 'Charlie', 'David']);

  useEffect(() => {
    const newUser = users;
    newUser.push('Eve');
    setUsers(newUser);
  }, []);

  const getUsers = () => users;

  return { setUsers, getUsers };
}

export default useUser;

useUser.tsx

จากโค๊ดจะเห็นได้ว่า เราได้นำโค๊ดในส่วนของฟังก์ชันการทำงานที่เคยอยู่ใน userLists component ออกมาไว้ที่ไฟล์ใหม่แล้ว

และผมได้เพิ่มฟังก์ชัน getUser เข้ามา เพื่อจะใช้เป็นตัวอย่างในการทำ stub ข้อมูลของฟังก์ชันนี้ เพื่อแสดงให้เห็นวิธีการทำงานของมัน

จากนั้นก็ import useUser เข้ามายังไฟล์ UserLists.tsx และเรียกใช้งาน เพื่อ get ข้อมูลมาแสดงผล

import useUser from './useUser';

const UserLists = () => {
  const { getRandomNumber } = useUser();
  const users = getRandomNumber();

  return (
    <div>
      {users.map((user, i) => (
        <p key={i}>{user}</p>
      ))}
    </div>
  );
};

export default UserLists;

UserLists.tsx

มาถึงตรงนี้ เราก็ยังไม่สามารถทดสอบการเรียกใช้งานฟังก์ชันภายในไฟล์ useUser.tsx ที่ถูกเรียกใช้ใน UseLists Component ได้ เพราะเนื่องจากการทำงานของ Cypress จะไม่สามารถเข้าไปเรียกฟังก์ชันที่ใช้ภายใน component ได้ตรงๆ ถ้าเราจะทดสอบฟังก์ชันใดๆ ก็ตาม สามารถทำได้เฉพาะวิธีการส่งฟังก์ชันผ่าน Props เท่านั้น จึงต้องมีการปรับเพิ่มวิธีการเรียกใช้งานไฟล์ useUser เพิ่มเติม

โดยเราจะทำให้ useUser สามารถส่งผ่าน Props มาได้ โดยกำหนดพารามิเตอร์ useUsers ขึ้นมา และกำหนดให้ไฟล์ useUserHook ที่ import เข้ามา เป็นค่า Default ถ้าไม่มีการส่งข้อมูลผ่าน Props มาให้

import useUserHook from './useUser';

type UserListsType = {
  useUsers: Function;
};

const UserLists = ({ useUsers = useUserHook }: UserListsType) => {
  const { getRandomNumber } = useUsers();
  const users = getRandomNumber();

  return (
    <div>
      {users.map((user, i) => (
        <p key={i}>{user}</p>
      ))}
    </div>
  );
};

export default UserLists;

UserLists.tsx

เท่านี้เราก็สามารถสร้างการทดสอบ เพื่อทดสอบฟังก์ชันภายใน component ได้แล้ว โดยในตัวอย่างจะเป็นการสร้าง stub ให้ return ค่า ['Bitmain', 'Spiderman', 'Antman'] แทนค่าเดิมในไฟล์ useUser

import UserLists from './UserLists';

describe('<UserLists />', () => {
  it('renders', () => {
    const useUserStub = () => {
      return {
        getUsers: cy.stub().as('getUsersStub').returns(['Bitmain', 'Spiderman', 'Antman']),
      };
    };

    cy.mount(<UserLists useUsers={useUserStub} />);

    cy.get('div').contains('Bitmain');
    cy.get('@getUsersStub').should('have.been.called');
  });
});

UserLists.cy.tsx

สรุป

เนื่องจาก cypress ไม่สามารถเข้าไปจัดการการทดสอบฟังก์ชันต่างๆ ภายใน component ตรงๆ ได้ เราจึงต้องใช้วิธีการส่งฟังก์ชันที่ต้องการจัดการทดสอบผ่าน Props แทน เท่านี้ก็สามารถทำการทดสอบได้แล้วนั้นเอง