เวลาที่เราใช้ NextJS บ่อยครั้งที่จะต้องมีการใช้ useRouter ที่เป็นของ next/navigation
เพื่อทำอะไรบ้างอย่าง
ทีนี้เมื่อเราต้องการเขียน component testing เพื่อที่ทดสอบ component ที่มี useRouter ประกอบอยู่ข้างในนั้นด้วยแล้ว cypress มันจะแสดงผล error ออกมาแบบนี้
![](https://nutshell.work/content/images/2024/02/Screenshot-2024-02-15-134147.png)
ก่อนอื่นต้องทำความเข้าใจก่อนว่า library บางตัวที่เราใช้งานอยู่นั้นมันอยู่ในรูปแบบของ context ซึ่งมันจะมีองค์ประกอบ 2 ส่วนใหญ่ๆ คือ Provider กับ Consumer
useRouter ของ next/navigation ก็เป็นเช่นเดียวกัน ซึ่งปกติแล้ว nextjs มันจะ build in provider มาให้ตั้งแต่ต้นแล้ว เราจึงสามารถเรียกใช้งาน useRouter ที่เป็นตัว consumer ได้เลยทันที (หาอ่านเรื่องของ react context เพิ่มเติมได้)
![](https://nutshell.work/content/images/2024/02/Screenshot-2024-02-15-140112.png)
ทีนี้ เมื่อเราต้องการที่จะทดสอบแค่ component ย่อยๆ มันก็เลยมีแค่การเรียกใช้ useRouter ที่เป็นตัว consumer เลยโดยที่จะยังไม่ได้ mount ตัว provider ของ next/navigation มันเลย เตือน Error นี้ขึ้นมา
![](https://nutshell.work/content/images/2024/02/Screenshot-2024-02-15-140319.png)
การ Mock Router ขึ้นมา
โดยทั่วไปแล้ว วิธีการแก้ไข้ที่ง่ายที่สุด คือ การสร้างตัว Mock Router ของ next/navigation ขึ้นมา เพื่อให้ component ที่ต้องการทดสอบสามารถเรียกใช้งานได้
ตัวอย่าง
สมมติว่าเรามี component ที่มีการ เรียกใช้งาน useRouter เพื่อเปลี่ยนไปยังหน้า about จะได้ code ประมาณนี้ (โค๊ดตัวอย่าง ใช้ Next.js เวอร์ชั่น 14.1.0)
Component
import React from 'react'
import { useRouter } from 'next/navigation'
type Props = {
link: string
children: React.ReactNode
}
const CustomLink = ({ children, link }: Props) => {
const router = useRouter()
const onClick = () => {
router.push(link)
}
return <button onClick={onClick}>{children}</button>
}
export default CustomLink
custom-link.tsx
import CustomLink from './custom-link'
const View = () => {
return (
<div>
<h1>Example</h1>
<p>This is an example page</p>
<CustomLink link='/about'>About</CustomLink>
</div>
)
}
export default View
view.tsx
สร้าง mock useRouter
เริ่มต้นจากไปสร้างไฟล์ router.js
ในโฟลเดอร์ cypress/utils/router.js
ซึ่งผมเลือกใช้ JavaScript แทน TypeScript จะได้ไม่ต้อง วุ่นวายกับ Type
import {
AppRouterContext,
AppRouterInstance
} from 'next/dist/shared/lib/app-router-context.shared-runtime'
const createRouter = (params) => ({
route: '/',
pathname: '/',
query: {},
asPath: '/',
basePath: '',
back: cy.spy().as('back'),
beforePopState: cy.spy().as('beforePopState'),
forward: cy.spy().as('forward'),
prefetch: cy.stub().as('prefetch').resolves(),
push: cy.spy().as('push'),
reload: cy.spy().as('reload'),
replace: cy.spy().as('replace'),
events: {
emit: cy.spy().as('emit'),
off: cy.spy().as('off'),
on: cy.spy().as('on')
},
isFallback: false,
isLocaleDomain: false,
isReady: true,
defaultLocale: 'fr',
domainLocales: [],
isPreview: false,
...params
})
const MockRouter = ({ children, ...props }) => {
const router = createRouter(props)
return (
<AppRouterContext.Provider value={router}>
{children}
</AppRouterContext.Provider>
)
}
export default MockRouter
cypress/utils/router.js
วิธีใช้งาน
เมื่อเรามีไฟล์ mock router แล้ว เราสามารถเรียกมันมาใช้งานได้ง่าย โดยเอา MockRouter การครอบ component ที่เราต้องการทดสอบไว้
import CustomLink from './custom-link'
import MockRouter from '../../../cypress/utils/router'
describe('<CustomLink />', () => {
it('renders', () => {
cy.mount(
<MockRouter>
<CustomLink link='/about'>About</CustomLink>
</MockRouter>
)
})
})
custom-link.cy.tsx
ถ้าหากเราต้องการตรวจสอบว่ามีการเรียก router.push(link)
จริงไหม สามารถทำได้โดยการใช้ @Alias
ที่กำหนดไว้ push: cy.spy().as('push')
ใน cypress/utils/router.js
import CustomLink from './custom-link'
import MockRouter from '../../../cypress/utils/router'
describe('<CustomLink />', () => {
it('renders', () => {
cy.mount(
<MockRouter>
<CustomLink link='/about'>About</CustomLink>
</MockRouter>
)
cy.get('button').click()
cy.get('@push').should('be.calledWith', '/about')
})
})
custom-link.cy.tsx
เท่านี้เราก็สามารถทดสอบ component นี้ได้แล้ว