使用Jest测试含API调用的UserModel类时Mock报错排查
遇到的错误:
TypeError: mockHttp.post.mockResolvedValueOnce is not a function
测试文件代码
import { IUserApiClientUrls } from '../../../src/interfaces' import { UserModel } from '@/models/UserModel' import httpClient from '@/services/index' import apiClient from '@/services/server.json' jest.mock('@/services/index') const mockHttp = httpClient as jest.Mocked<typeof httpClient> const createSut = () => { const urlUser: IUserApiClientUrls = apiClient.user return new UserModel(urlUser) } afterEach(() => { jest.clearAllMocks() }) test.only('Given an email and password When executed user login Then the response must have a tokenGiven an email and password When executed user login Then the response must have a token', async () => { const name = 'Jolie' const email = 'teste@gmail.com' const password = '123456' const dataToken = { data: { token: '123.123.123abcd' }, } const sut = createSut() mockHttp.post.mockResolvedValueOnce(dataToken) const response = await sut.register(name, email, password) expect(mockHttp.post).toHaveBeenCalled() expect(response).toHaveProperty('token') })
UserModel类代码
import { IUser, IUserApiClientUrls, IUserLogged, IUserModelApi, IUserApiRegister } from '@/interfaces' import httpClient from '@/services/index' export class UserModel implements IUserModelApi { constructor(private readonly url: IUserApiClientUrls) {} async register(name: string, email: string, password: string): Promise<IUserApiRegister> { const params = { url: this.url.register, payload: { name, email, password, }, } return httpClient .post(params.url, params.payload) .then((response) => response.data) .catch((e) => console.error(e)) } }
基于axios.create()实现的httpClient代码
import axios, { AxiosInstance } from 'axios' const API_ENVS = { production: '', development: '', local: 'http://localhost:3000', } const httpClient: AxiosInstance = axios.create({ baseURL: API_ENVS.local, }) httpClient.interceptors.response.use( (response) => response, (error) => { return error } ) export default httpClient
用户疑问:我也尝试直接Mock axios但未成功,请问我哪里操作出错了?
问题原因与解决方案
问题原因
jest.mock('@/services/index')会自动生成一个空的mock模块,但httpClient是axios.create()创建的AxiosInstance实例,其post等方法并不是jest的mock函数。你通过as jest.Mocked<typeof httpClient>做类型断言只是告诉TypeScript这是一个mock对象,但实际运行时mockHttp.post并没有被替换成可调用mockResolvedValueOnce的jest mock函数,因此报错。
解决方案一:手动Mock httpClient的方法
在jest.mock之后,手动将httpClient的post方法赋值为jest mock函数:
修改测试文件中的mock部分:
jest.mock('@/services/index') const mockHttp = httpClient as jest.Mocked<typeof httpClient> // 手动把post设置为jest mock函数 mockHttp.post = jest.fn()
测试用例中的逻辑无需改动,此时mockHttp.post已经是标准的jest mock函数,可以正常调用mockResolvedValueOnce。
解决方案二:Mock axios模块本身
因为httpClient是通过axios.create()创建的,你可以直接mock axios模块,让create方法返回带有mock请求方法的实例:
修改测试文件:
import axios from 'axios' import { IUserApiClientUrls } from '../../../src/interfaces' import { UserModel } from '@/models/UserModel' import httpClient from '@/services/index' import apiClient from '@/services/server.json' // Mock axios整个模块 jest.mock('axios') const mockAxios = axios as jest.Mocked<typeof axios> const createSut = () => { const urlUser: IUserApiClientUrls = apiClient.user return new UserModel(urlUser) } afterEach(() => { jest.clearAllMocks() }) test.only('Given an email and password When executed user login Then the response must have a token', async () => { const name = 'Jolie' const email = 'teste@gmail.com' const password = '123456' const dataToken = { data: { token: '123.123.123abcd' }, } // 让axios.create返回带有mock post方法的实例 mockAxios.create.mockReturnValue({ post: jest.fn().mockResolvedValueOnce(dataToken), interceptors: { response: { use: jest.fn() } } } as any) const sut = createSut() const response = await sut.register(name, email, password) expect(mockAxios.create).toHaveBeenCalled() expect(mockAxios.create().post).toHaveBeenCalledWith(apiClient.user.register, { name, email, password }) expect(response).toHaveProperty('token') })
额外注意点
UserModel的register方法中,catch块只做了console.error(e),没有抛出错误,这样如果请求失败,测试无法捕获到异常。建议修改为:
.catch((e) => { console.error(e) throw e // 抛出错误,让调用方可以处理 })
内容的提问来源于stack exchange,提问作者Merieli Manzano

