ในการทดสอบ component นั่น หากใน component มีฟังก์ชั่น หรือ logic อะไรบางอย่างอยู่ใน component นั้นด้วย มันทำให้การทดสอบ component นั่นๆ จะไปเรียกฟังก์ชั่นเหล่านั้น
จากบทความก่อนหน้านี้ https://nutshell.work/how-to-testing-custom-hooks-in-testing-components-with-cypress/
หากเราไม่ต้องการให้มีการทำงานภายในฟังก์ชั่นที่มีการเรียกใช้งาน เราสามารถ Mock ตัว stub/spy ของฟังก์ชั่นเหล่านี้ได้
ปรับวิธีการเรียกใช้งาน Custom Hook
ก่อนอื่นต้องบอกก่อนว่า ถ้าหากเราต้องการที่จะ stub ฟังก์ชั่นใน hook เราไม่สามารถเข้าไป stub มันผ่านทาง component ได้ตรงๆ มันต้องมีการปรับวิธีการเรียกใช้งาน hook ใหม่สักหน่อย ซึ่งต้องบอกก่อนว่า ผมใช้วิธีนี้อยู่ ในอนาคตหากมีวิธีที่ดีกว่า ค่อยว่ากัน
เริ่มต้นที่ เรามี component และ hook ของเราจากบทความที่แล้ว คือ component สำหรับเพิ่มค่าบวก ลบ ซึ่งมีโค๊ดดังนี้
import useCounter from './useCounter'
const Counter = () => {
const { count, increment, decrement } = useCounter()
return (
<div>
<h1>Count: {count}</h1>
<button id='incrementButton' onClick={increment}>
-
</button>
<button id='decrementButton' onClick={decrement}>
+
</button>
</div>
)
}
export default Counter
counter.tsx
import { useCallback, useState } from 'react'
export type UseCounterReturnType = {
count: number
increment: () => void
decrement: () => void
}
const useCounter = (): UseCounterReturnType => {
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 React from 'react'
import Counter from './counter'
describe('<Counter />', () => {
it('Stub custom hook', () => {
cy.mount(<Counter />)
cy.get('h1').contains('Count: 0')
cy.get('#incrementButton').click()
cy.get('h1').contains('Count: 1')
})
})
counter.cy.tsx
สิ่งที่ต้องทำเลยคือ เราจะต้องทำยังไงก็ได้ ให้ตัว component ไปเรียกใช้งาน useCount ที่เราได้ทำการ Mock ตัว stub/spy มันเอาไว้
สร้าง Stub ของ Hook
เริ่มต้นจากเราต้องสร้างตัว Mock ตัว stub/spy ของ hook ขึ้นมาก่อน ซึ่งเราจะสร้างไว้ที่ counter.cy.tsx
const useCounterStub = () => {
return {
count: 0,
increment: cy.stub().as('increment'),
decrement: cy.stub().as('decrement'),
}
}
counter.cy.tsx
จากนั้นก็มาถึงขึ้นตอนที่จะต้องมีการปรับการเรียกใช้งาน hook ใน component แล้ว โดยจากเดิมเป็นการเรียกใช้งานตรงๆ ผ่านการ import ไฟล์แล้วใช้งานเลย
เราจะเปลี่ยนเป็น ส่งค่า hook ผ่านทาง component props แทน แบบนี้
it('Stub custom hook', () => {
const useCounterStub = () => {
return {
count: 0,
increment: cy.stub().as('increment'),
decrement: cy.stub().as('decrement')
}
}
cy.mount(<Counter useCounter={useCounterStub} />)
})
counter.cy.tsx
ใส่ใน component ก็จะต้องเพิ่ม props ที่ใช้รับค่า useCounter ที่ส่งมาด้วย
import { UseCounterReturnType } from './useCounter'
type CounterProps = {
useCounter: () => UseCounterReturnType
}
const Counter = ({ useCounter }: CounterProps) => {
const { count, increment, decrement } = useCounter()
return (
<div>
<h1>Count: {count}</h1>
<button id='incrementButton' onClick={increment}>
-
</button>
<button id='decrementButton' onClick={decrement}>
+
</button>
</div>
)
}
export default Counter
counter.tsx
เท่านี้เรา ก็สามารถที่จะ Mock ตัว stub/spy ฟังก์ชั่นต่างๆ ภายใน hooks ได้โดยที่ไม่จำเป็นต้องไปเรียกใช้งานคำสั่งต่างๆ ภายในฟังก์ชันเลย
import React from 'react'
import Counter from './counter'
describe('<Counter />', () => {
it('Stub custom hook', () => {
const useCounterStub = () => {
return {
count: 0,
increment: cy.stub().as('increment'),
decrement: cy.stub().as('decrement')
}
}
cy.mount(<Counter useCounter={useCounterStub} />)
cy.get('#incrementButton').click()
cy.get('@increment').should('have.been.called')
})
})
counter.cy.tsx

กำหนดค่า Default
ถึงแม้ว่า เราจะสามารถ Mock ตัว stub/spy ค่าต่างๆ ภายใน hook ได้แล้ว แต่ว่า ยังมีส่วนที่ต้องเพิ่มเติมเข้าไปอีกหน่อย
นั่นคือ สังเกตุว่า ถ้าหากเราต้องการเรียกใช้งาน component เราจะต้อง ส่ง hook ไปด้วยเสมอ ซึ่งอาจจะเพิ่มความยุ่งยากได้
วิธีแก้คือ ให้กำหนดค่า default ของ parameter ของ useCounter ใน component props ให้ไปให้ useCounter จริงๆ ที่ไม่ใช้ ตัว Mock stub/spy
import _useCounter from './useCounter'
const Counter = ({ useCounter = _useCounter }: CounterProps) => {
// ...
})
เพิ่ม _useCount เป็นค่า default in counter.tsx
รวม code ทั้งหมด ก็จะได้แบบนี้
import _useCounter, { UseCounterReturnType } from './useCounter'
type CounterProps = {
useCounter: () => UseCounterReturnType
}
const Counter = ({ useCounter = _useCounter }: CounterProps) => {
const { count, increment, decrement } = useCounter()
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 แบบเดิมได้แล้ว และสามารถสร้าง Mock ตัว stub/spy ได้โดยไม่กระทบการทำงานของ component