ในการพัฒนาเว็บไซต์ด้วย React หลีกเลี่ยงไม่ได้ที่ในส่วนประกอบต่างๆ อาจจำเป็นต้องมีการพึ่งพา Package จากภายนอก เช่น context, props หรืออะไรก็ตามเพื่อให้ทำงานได้อย่างถูกต้อง และส่วนนี่เป็นปัญหาที่เกิดในทุกๆ การทำการสอบ ซึ่งใน cypress ก็มีวิธีการจัดการกับการทำงานในส่วนนี้ไว้ด้วยแล้วเหมือนกัน

เรามาดูตัวอย่างที่เข้าใจง่ายๆ กัน:

Dark & Light Mode

บทความนี้ผมจะยก Code ง่ายๆ ของการใช้งาน Context ใน React มากให้ดูเป็นตัวอย่าง โดยจะสร้างปุ่มง่ายๆ ที่จะมีการเรียกใช้งาน darkMode ผ่าน context ที่ถูกเขียนไว้ที่อื่นในเว็บแอปพลิเคชัน โดยจะเรียกไป hook useDarkMode ซึ่งกำหนดไว้ในโฟลเดอร์อื่นอีกทีนึง

Dark Mode Selector

เริ่มต้นจากการสร้างปุ่ม ที่ทำหน้าที่เปลี่ยน Mode สลับไปมาจาก dark mode เป็น light mode ซึ่งในนี้เป็นตัวอย่างอย่างง่าย เลยให้มันทำหน้าที่แค่เปลี่ยน Mode และข้อความบนปุ่มแทน

import { useDarkMode } from './useDarkMode';

export const DarkModeSelector = () => {
  const { darkMode, setDarkMode } = useDarkMode();
  return (
    <button onClick={() => setDarkMode(!darkMode)}>
      {darkMode ? 'Switch to lightmode' : 'Switch to darkmode'}
    </button>;
};

export default DarkModeSelector;

DarkModeSelector.tsx

useDarkMode

ต่อมาเราจะสร้างส่วนของการใช้งาน Context โดย จะกำหนด DarkModeProvider ที่รับ Component เข้ามา เพื่อให้สามารถใช้งาน Context ได้

import React, { createContext, useContext } from 'react';

const DarkModeContext = createContext(undefined);

interface DarkModeProviderTypes {
  children: React.ReactNode;
  value: {
    darkMode: boolean;
    setDarkMode: Function;
  };
}

export const DarkModeProvider = ({ children, value: { darkMode, setDarkMode } }: DarkModeProviderTypes) => (
  <DarkModeContext.Provider value={{ darkMode, setDarkMode }}>{children}</DarkModeContext.Provider>
);

export const useDarkMode = () => {
  const context = useContext(DarkModeContext);
  if (!context) {
    throw new Error('useDarkMode must be used within a DarkModeProvider');
  }
  return context;
};

useDarkMode.tsx

ซึ่งหาการเราลองสร้างไฟล์ทดสอบ แล้วลองเรียกใช้งาน DarkModeSelector โดยตรง เราจะพบว่า Cypress จะแสดง Error ขึ้นมา

import React from 'react'
import DarkModeSelector from './DarkModeSelector'

describe('<DarkModeSelector />', () => {
  it('renders', () => {
    // see: https://on.cypress.io/mounting-react
    cy.mount(<DarkModeSelector />)
  })
})

DarkModeSelector.cy.tsx

Error: useDarkMode must be used within a DarkModeProvider

Error: useDarkMode must be used within a DarkModeProvider

เนื่องจาก DarkModeSelector พยายามใช้ Context ที่ไม่ได้ถูกกำหนดไว้ก่อนที่จะทำการ mounting

วิธีที่ง่ายที่สุดและง่ายที่สุดในการแก้ไข คือ การสร้างตัว wrapper จากนั้นจึง wrap ส่วนของ Component ของเราไว้ในฟังก์ชัน mount

import React, { useState } from 'react';
import { DarkModeProvider } from './useDarkMode';

type DarkModeWrapperTypes = {
  children: React.ReactNode;
};

const DarkModeWrapper = ({ children }: DarkModeWrapperTypes) => {
  const [darkMode, setDarkMode] = useState(false);

  return (
    <DarkModeProvider
      value={{
        darkMode,
        setDarkMode,
      }}
    >
      {children}
    </DarkModeProvider>
  );
};

export default DarkModeWrapper;

DarkModeWrapper.tsx

จากนั้นให้เรียกใช้งาน DarkModeWrapper ในจังหวะ mounting ได้เลย

import DarkModeSelector from './DarkModeSelector';
import DarkModeWrapper from './DarkModeWrapper';

describe('<DarkModeSelector />', () => {
  it("renders the 'Switch to darkmode'", () => {
    cy.mount(
      <DarkModeWrapper>
        <DarkModeSelector />
      </DarkModeWrapper>
    );
    cy.contains('Switch to darkmode');
  });
})

DarkModeSelector.cy.tsx

เท่านี้เราก็สามารถทดสอบไฟล์ Component ที่มีการเรียกใช้งาน Context ได้แล้ว

การส่งค่าผ่าน Props

จากโค๊ดด้านบน สมมติว่า ถ้าหากเราต้องการส่งค่า initialValue ไปยัง DarkModeWarpper เพื่อกำหนดค่าเริ่มต้นของ mode ก็สามารถทำได้ง่าย

import React, { useState } from 'react';
import { DarkModeProvider } from './useDarkMode';

type DarkModeWrapperTypes = {
  children: React.ReactNode;
  initialValue?: boolean;
};

const DarkModeWrapper = ({ children, initialValue }: DarkModeWrapperTypes) => {
  const [darkMode, setDarkMode] = useState(initialValue || false);

  return (
    <DarkModeProvider
      value={{
        darkMode,
        setDarkMode,
      }}
    >
      {children}
    </DarkModeProvider>
  );
};

export default DarkModeWrapper;

DarkModeWrapper.tsx

การใช้งานร่วมกับ Spy

จากสิ่งที่เราทำมาทั้งหมด เป็นเพียงแค่การทดสอบว่า context มีการทำงานที่ถูกต้องหรือไม่ แต่เราไม่สามารถรู้ได้เลยว่า ปุ่ม Button นั้น ได้เรียกใช้งาน setDartMode หรือเปล่า?

ในส่วนนี้เราจะประยุกต์การใช้งานระหว่างตัว warpper context และ การใช้งาน spy ใน cypress เพื่อให้สามารถเข้าไปทดสอบการเรียกใช้งานฟังก์ชั้น setDartMode ใน DarkModeSelector ได้

โดยไปที่ไฟล์ DarkModeWrapper.tsx เราจะทำการเพิ่มฟังก์ชั้น initDarkModeWrapper ขึ้นมา ซึ่งจะเอามันมาครอบฟังก์ชั่น DarkModeWrapper อีกทีนึง

จากนั้นก็จะสร้าง setDarkModeSpy และจัดการ Return ค่าฟังชั่น DarkModeWarpper และ setDarkModeSpy ออกไปใช้งาน ดังในตัวอย่าง

import React, { useState } from 'react';
import { DarkModeProvider } from './useDarkMode';

type DarkModeWrapperTypes = {
  children: React.ReactNode;
  initialValue?: boolean;
};

const initDarkModeWrapper = () => {
  const setDarkModeSpy = cy.spy();

  const DarkModeWrapper = ({ children, initialValue }: DarkModeWrapperTypes) => {
    const [darkMode, setDarkMode] = useState(initialValue);
    return (
      <DarkModeProvider
        value={{
          darkMode,
          // setDarkMode,
          setDarkMode: (value: boolean) => {
            setDarkMode(value);
            setDarkModeSpy(value);
          },
        }}
      >
        {children}
      </DarkModeProvider>
    );
  };

  return [setDarkModeSpy, DarkModeWrapper];
};

export default initDarkModeWrapper;

DarkModeWrapper.tsx

จากนั้นเราจะเข้าไปปรับไฟล์ทดสอบ DarkModeSelector.cy.tsx โดยจะเปลี่ยนมาเรียกใช้งาน DarkModeWarpper จากฟังก์ชั้น initDarkModeWrapper อีกทีนึง

import DarkModeSelector from './DarkModeSelector';
import initDarkModeWrapper from './DarkModeWrapper';

describe('<DarkModeSelector />', () => {
  it("renders the 'Switch to darkmode'", () => {
    const [, DarkModeWrapper] = initDarkModeWrapper();
    cy.mount(
      <DarkModeWrapper>
        <DarkModeSelector />
      </DarkModeWrapper>
    );
    cy.contains('Switch to darkmode');
  });

  it("changes context value to 'true' once clicked when initiated with darkmode=false value", () => {
    const [setDarkModeSpy, DarkModeWrapper] = initDarkModeWrapper();
    cy.mount(
      <DarkModeWrapper initialValue={false}>
        <DarkModeSelector />
      </DarkModeWrapper>
    );

    cy.get('button').click();
    cy.wrap(setDarkModeSpy).should('have.been.called');
  });
})

DarkModeSelector.cy.tsx

เท่านี้เราก็สามารถทดสอบฟังก์ชั่น setDarkMode ได้แล้วว่า มีการเรียกใช้งานได้อย่างถูกต้องหรือไม่