เริ่มต้นที่ ทำความเข้าใจเกี่ยวกับ Test Double กันก่อน

Mock
ใน jest นั้น mock จะทำหน้านี้เป็นทั้ง mock, stub และ dummy ไปเลย แบบจบในตัวมันเลย เพราะฉนั้นหากเราพูดถึง mock ใน jest มันก็ คือ รวมทั้งหมดของ test double เลย ยกเว้น spy ไว้ตัวนึงที่ใน jest มีให้
ปกติแล้วเราจะใช้ mock เมื่อต้องการให้ code ของเราสามารถทำงานได้ โดยไม่ต้อง ไปเรียกใช้งาน function จริงๆ จากไฟล์อื่นๆ
มาลอง mock function กัน
Mocking function
สมมติว่าเรามีไฟล์ calculate.ts
ที่มีการไปเรียกใช้งานฟังก์ชั่น add()
จาก ไฟล์ math.ts
อีกทีนึง
โดยเราจะเขียนโค๊ดง่ายๆ เลย คือ ให้ฟังก์ชั่น doubleSum()
รับค่า x
และ y
จากนั้นไปเรียกใช้งานฟังก์ชั่น add()
ที่อยู่ในไฟล์ math.ts
แล้วเอาค่าที่ได้จาก add()
มาบวกกันอีกที
import { add } from '../math'
export const doubleSum = (x: number, y: number): number => {
const sum = add(x, y)
return x + y + sum
}
calculate.ts
export const add = (x: number, y: number): number => x + y
math.ts
ทีนี้หากเราต้องการที่จะเขียนการทดสอบ เพื่อทดสอบ doubleSum()
ก็สามารถเรียกใช้งานฟังก์ชั่น doubleSum()
ได้ เลย
import { doubleSum } from './calculate'
test('should return 6 when call doubleSum and send 1 and 2', () => {
const numberOne = 1
const numberTwo = 2
// 6 = 1 + 2 + 1 + 2
const expected = 6
const result = doubleSum(numberOne, numberTwo)
expect(result).toBe(expected)
})
calculate.test.ts
เวลาทำงานจริง ถ้าโค๊ดเราตรงมาแบบนี้มันก็ดีสิ แต่ในความเป็นจริงๆ แล้ว เราต้องระบุ scope ให้ได้ก่อนว่าเราต้องการจะทดสอบอะไร ตรงไหน
ในตัวอย่างๆ จริงๆ แล้วเราต้องการทดสอบว่า ฟังก์ชั่น doubleSum()
นั้นทำงานได้ถูกต้องหรือเปล่า โดยเราไม่ได้สนใจว่า ฟังก์ชั่น add()
ของ math.ts
นั้นทำงานถูกต้องไหม เพราะเราได้ทดสอบมันแยกไปแล้ว
เราจึงจำเป็นที่ต้องตัดการทำงานของ add()
ออก เพื่อจะทดสอบให้แน่ใจว่า code ในฟังก์ชั่น doubleSum()
ทำงานได้ถูกต้องไหม
import { add } from '../math'
export const doubleSum = (x: number, y: number): number => {
// ...
return x + y + sum // เราสนใจตรงนี้
}
ในส่วนนี้แหละ mock จะเข้ามามีส่วนร่วมกับการทดสอบ
เริ่ม mock function ทายใน math.ts
กัน
jest.mock('../../../src/function/math', () => ({
add: jest.fn()
}))
mock math.ts
หากเราต้องการกำหนดค่าที่จะให้ add()
return กลับไป ก็สามารถใช้ jest.fn().mockReturnValue(ค่าที่ต้องการให้ return)
ได้ หรือจะใช้ jest.fn().mockImplementation((x, y) => x + y)
import { doubleSum } from './calculate'
// Implement in Mock
jest.mock('../../../src/function/math', () => ({
add: jest.fn().mockImplementation((x, y) => {
if (x === 1 && y === 2) return 20
return x + y
})
}))
describe('Basic Mock solution 1', () => {
afterEach(() => {
jest.restoreAllMocks()
})
test('should return 23 when call doubleSum and send 1 and 2', () => {
const numberOne = 1
const numberTwo = 2
// if not mock return value (1 + 2) + (1 + 2) = 6
const expected = 23
const result = doubleSum(numberOne, numberTwo)
expect(result).toBe(expected)
})
test('should return 23 when call doubleSum and send 10 and 2', () => {
const numberOne = 10
const numberTwo = 2
const expected = 24
const result = doubleSum(numberOne, numberTwo)
expect(result).toBe(expected)
})
})
calculate.test.ts
หากเราต้องการที่จะ implement ตัว return ใน mock ของเราให้แยกเป็นเป็นแต่ละการทดสอบเลย เราสามารถทำได้แบบนี้
import { doubleSum } from './calculate'
// 1. Import Math.ts
import * as math from './math'
// 2. Create mock
jest.mock('../../../src/function/math')
describe('Basic Mock solution 2', () => {
test('should return 20 when call doubleSum and send 1 and 2', () => {
// Arrange
const numberOne = 1
const numberTwo = 2
const expected = 20
// 3. Implement Mock in it.
const addMock = math.add as jest.Mock
addMock.mockReturnValue(17)
const total = doubleSum(numberOne, numberTwo)
expect(addMock).toHaveBeenCalled()
expect(total).toBe(expected)
})
test('should return 110 when call doubleSum and send 1 and 2', () => {
// Arrange
const numberOne = 99
const numberTwo = 1
const expected = 100
// 3. Implement Mock in test
const addMock = math.add as jest.Mock
addMock.mockReturnValue(0)
const total = doubleSum(numberOne, numberTwo)
expect(addMock).toHaveBeenCalled()
expect(total).toBe(expected)
})
})
mock add() in test
หาเราใช้ javascript ก็ไม่จำเป็นต้องใส่ as jest.Mock
ต่อท้าย const addMock = math.add
ถ้าอยากตรวจสอบว่า addMock
มีการเรียกใช้งานหรือเปล่าให้ใช้ expect(addMock).toHaveBeenCalled()
ตรวจสอบว่า addMock
มีการเรียกที่ครั้ง ให้ใช้ expect(addMock).toHaveBeenCalledTimes(1)
และต้องเพิ่ม jest.restoreAllMocks()
เข้าไปด้วย เพื่อให้มันเคลียค่า mock ที่เก็บไว้ในแต่ละครั้ง ไม่อย่างอย่างนั้นมันจะนับจำนวนครั้งไปเรื่อยๆ
afterEach(() => {
jest.restoreAllMocks()
})
restore mock
Mock library
นอกจากที่จะ mock ฟังก์ชั่นที่เราได้เขียนขึ้นเองแล้ว บางครั้งเราอาจจำเป็นต้อง mock library บางตัวเพื่อคุม output ที่ได้จาก library ที่ใช้งานอยู่ mock ใน jest นั้นก็สามารถทำได้เหมือนกัน
เช่น สมมติว่า เรามีฟังก์ชั่น getUserByUserName()
ที่มีการเรียกใช้งานฟังก์ชั่น findIndex()
ที่เป็นของ lodash
แบบนี้
import _ from 'lodash'
export const getUserByUserName = (keyword: string) => {
var users = [
{ user: 'barney', birthday: '1980-01-01', age: 47 },
{ user: 'fred', birthday: '1990-01-01', age: 37 },
{ user: 'pebbles', birthday: '2000-01-01', age: 27 }
]
const index = _.findIndex(users, (o) => {
return o.user == keyword
})
return users[index]
}
user.ts
ที่นี้สมมติว่าเราไม่ต้องการให้ getUserByUserName()
ไม่ต้องไปเรียก findIndex()
จริง และกำหนดให้ return ค่าที่เราต้องการใช้ในแต่ละชุดการทดสอบเลย
import _ from 'lodash'
import { getUserByUserName } from './user'
// Normal without mock will throw error
// jest.mock('lodash')
describe('mock', () => {
afterEach(() => {
jest.restoreAllMocks()
})
// 1. Normal without mock
it('Should be index from keyword without mock', () => {
const keyword = 'barney'
const actual = getUserByUserName(keyword)
expect(actual).toMatchObject({
user: 'barney',
birthday: '1980-01-01',
age: 47
})
})
// 2. With mock
it('Should be index from keyword with mock', () => {
const keyword = 'barney'
// Mock lodash
jest.mock('lodash')
const mockFindIndex = jest.fn().mockReturnValue(2) // pebbles
_.findIndex = mockFindIndex // Override lodash (optional, if needed)
const actual = getUserByUserName(keyword)
expect(mockFindIndex).toHaveBeenCalled()
expect(actual).toMatchObject({
user: 'pebbles',
birthday: '2000-01-01',
age: 27
})
})
})
user.test.ts
นี้เป็นตัวอย่างการ mock อย่างง่ายๆ และ mock นั้นมีฟังก์ชั่นให้ได้ลองใช้กันอีกเยอะสามารถเข้าไปดูได้ที่ document ของ jest ได้เลย
อีกอย่าง mock จะใช้กับการจำลองการทำงานของไฟล์อื่นๆ ซึ่งมันจะไม่สามารถ mock ฟังก์ชั่นในไฟล์มันเองได้ หากเราต้องการ mock ฟังก์ชั่นในไฟล์เดียวกันกับที่ต้องการจะทดสอบเราจะไปใช้ spy
แทนการใช้งาน mock เดี๋ยวจะพูดถึงในบทความถัดไป
