เวลาที่เราพัฒนาเว็บไซต์ด้วย React หรือ NextJS นั้น เป็นเรื่องปกติที่ต้องมีการประกาศฟังก์ชั่นบางอย่างภายใน component ไม่ว่าจะเป็น ฟังก์ชั่นที่เราสร้างขึ้นมาเอง เพื่อทำงานบางอย่าง หรือ ฟังก์ชั่น hooks ต่างๆ ที่ library มีใช้เรียกใช้งาน

หน้าตามันก็จะประมาณนี้

import { useCallback, useState } from 'react'

const Counter = () => {
  const [count, setCount] = useState(0)

  const increment = useCallback(() => setCount((x) => x + 1), [])
  const decrement = useCallback(() => setCount((x) => x - 1), [])

  return (
    <div>
      <h1>Count: {count}</h1>
      <button id='incrementButton' onClick={increment}>
        -
      </button>
      <button id='decrementButton' onClick={decrement}>
        +
      </button>
    </div>
  )
}

export default Counter

counter.tsx

ซึ่งถ้าหากว่าเราต้องการทดสอบพฤติกรรมของ component นี้ โดยใช้ Cypress ก็สามารถทำได้ โดยง่าย

import React from 'react'
import Counter from './counter'

describe('<Counter />', () => {
  it('renders', () => {
    cy.mount(<Counter />)

    cy.get('h1').contains('Count')
  })
})

counter.cy.tsx

หรือถ้าหากอยากที่จะทดสอบพฤติกรรมการทำงานของ component นี้ก็สามารถทำได้ด้วยเช่นกัน

import React from 'react'
import Counter from './counter'

describe('<Counter />', () => {
  it('renders', () => {
    cy.mount(<Counter />)

    cy.get('h1').contains('Count: 0')

    cy.get('#incrementButton').click()

    cy.get('h1').contains('Count: 1')
  })
})

counter.cy.tsx

แล้วถ้าหากว่าเราอยากจะทดสอบการทำงานเฉพาะ custom hook หละ จะทำยังไง... หลังจากที่ลองทำดูก็พบวิธีการทดสอบ


Custom Hook

ซึ่งอย่างที่รู้กัน (มั้งนะ) ว่าเราสามารถที่จะแยกเอาส่วน hooks ของ library และฟังก์ชั่นต่างๆ ที่เราเขียนไว้ แยกออกมาเป็น custom hook อีกตัวหนึ่งได้ เพื่อให้ง่ายต่อการใช้งานและปรับปรุงแก้ไขในอนาคต (และทดสอบ)

import { useCallback, useState } from "react"

const useCounter = () => {
  const [count, setCount] = useState(0)
  
  const increment = useCallback(() => setCount((x) => x + 1), [])
  const decrement = useCallback(() => setCount((x) => x - 1), [])

  return {
    count,
    increment,
    decrement
  }
}

export default useCounter

useCounter.tsx

import useCounter from "./useCounter"

const Counter = () => {
  const { count, increment, decrement } = useCounter()

  return (
    <div>
      <h1>Count: {count}</h1>
      <button onClick={decrement}>-</button>
      <button onClick={increment}>+</button>
    </div>
  );
}

export default Counter

counter.tsx

เท่านี้เราก็สามารถเริ่มทดสอบ ฟังก์ชั่นต่างๆ ภายใน useCounter ได้แล้ว


ทดสอบ Custom Hook ผ่าน Component

โดยปกติแล้ว ถ้าหากเราต้องการทดสอบ hook เราจะต้องทำสอบผ่านทาง component แบบนี้

import React from 'react'
import Counter from './counter'
import useCounter, { UseCounterReturnType } from './useCounter'

describe('<Counter />', () => {
  it('renders', () => {
    cy.mount(<Counter />)

    cy.get('h1').contains('Count: 0')

    cy.get('#incrementButton').click()

    cy.get('h1').contains('Count: 1')
  })

})

counter.cy.tsx

แล้วถ้าเราอยากทดสอบ custom hook โดยไม่ผ่าน component จริงๆ ที่เรียกมใช้งาน ก็สามารถทำได้โดยการ Mock Component ขึ้นมาเพื่อ เรียกใช้งาน hooks ที่ต้องการทดสอบ

import React from 'react'
import Counter from './counter'
import useCounter, { UseCounterReturnType } from './useCounter'

describe('<Counter />', () => {
  it('renders only useCounter', () => {
    // Arrange
    let counter: UseCounterReturnType

    const MockComponent = () => {
      counter = useCounter()
      return null
    }

    // Act
    cy.mount(<MockComponent />).then(() => {
      const count = counter.count

      // Assert
      expect(count).to.equal(0)
    })
  })
})

counter.cy.tsx

การทดสอบด้วยวิธีนี้ จะต่างจากวิธีแรก ตรงที่ ใน mock component จะ return ค่ากลับเป็น null แทน html และต้องสร้างตัวแปรขึ้นมาเพื่อรับค่าจาก hooks (useCounter) ด้วย

อีกอย่างที่แตกต่างกันคือ การ mount และ assert ค่า จะถูกเปลี่ยนเป็น assert ใน then แทน