如何使用Jasmine编写含API调用的组件点击功能单元测试?
问题描述
我在utils.tsx中定义了如下工具函数:
// resolveAxiosInstance creates an axios instance const createAxiosInstance = resolveAxiosInstance(); export const getItemList = async params => { const axios = await createAxiosInstance; const res = await axios.get("/my-url", {params}); return res.data; }
在mycomponent.tsx组件中使用该函数,按钮点击触发clickMe函数,设置loading状态后调用API,根据返回结果更新状态并渲染表格:
export const MyComponent = () => { // rest of component code const clickMe = () => { setIsLoading(true); const data = { // item and price are vars whose values are filled by user through input text itemName: item, itemPrice: price, }; getItemList(data).then(res => { if (res) { setItemData({ itemName: name, itemPrice: price, itemDiscount: res.disc, }); } }, err => console.log(err)); } return ( // rest of the component code <div> <Button onClick={clickMe} data-testid="update">Click Me</Button> </div> ) }
我尝试用Jasmine写单元测试,通过simulate("click")触发按钮点击,但无法正确执行API调用。使用spyOn时出现错误“getItemList is not declared configurable”,测试代码如下:
it("should show data on click me", () => { const wrapper = mount(<MyComponent />); let elem = wrapper.find(MyComponent); const mockSpy = Jasmine.createSpy("getItemList").and.returnValue(Promise.resolve(mockResp)) let btn = elem.find('[data-testid="update"]'); btn.at(0).simulate("click"); elem = elem.update(); expect(elem.find("table").length).toBe(1); });
请问如何正确编写该单元测试,验证按钮点击后调用API并根据返回结果更新视图的流程?
解决方案
1. 修复getItemList的模拟问题
出现“getItemList is not declared configurable”错误的核心原因是:ES模块导出的顶层函数默认不可配置,直接用spyOn无法修改它。有两种解决方式:
方式一:调整工具函数的导出结构
将utils.tsx的导出改为对象形式,让方法可被模拟:
// utils.tsx const createAxiosInstance = resolveAxiosInstance(); export const apiUtils = { getItemList: async params => { const axios = await createAxiosInstance; const res = await axios.get("/my-url", {params}); return res.data; } }
组件中对应修改调用方式:
import { apiUtils } from './utils.tsx'; // clickMe函数内修改为 apiUtils.getItemList(data).then(res => { // 原有逻辑 })
测试时直接模拟对象方法:
import { apiUtils } from './utils.tsx'; it("should show data after button click", async () => { const mockResp = { disc: 15 }; // 模拟API返回值 spyOn(apiUtils, 'getItemList').and.returnValue(Promise.resolve(mockResp)); const wrapper = mount(<MyComponent />); const updateBtn = wrapper.find('[data-testid="update"]'); // 触发按钮点击 updateBtn.simulate("click"); // 等待异步Promise执行完成 await Promise.resolve(); // 更新组件状态 wrapper.update(); // 验证表格是否渲染 expect(wrapper.find("table").length).toBe(1); // 验证API是否被正确调用(替换为组件中item/price的实际初始值) expect(apiUtils.getItemList).toHaveBeenCalledWith({ itemName: '', itemPrice: '' }); });
方式二:不修改原代码,动态修改导出属性
如果不想改动原有工具函数的结构,可以通过Object.defineProperty手动设置getItemList为可配置:
import * as utils from './utils.tsx'; it("should show data after button click", async () => { const mockResp = { disc: 15 }; // 重新定义getItemList,开启可配置 Object.defineProperty(utils, 'getItemList', { configurable: true, value: jasmine.createSpy('getItemList').and.returnValue(Promise.resolve(mockResp)) }); const wrapper = mount(<MyComponent />); const updateBtn = wrapper.find('[data-testid="update"]'); updateBtn.simulate("click"); await Promise.resolve(); wrapper.update(); expect(wrapper.find("table").length).toBe(1); expect(utils.getItemList).toHaveBeenCalled(); });
2. 异步测试的关键注意事项
- 必须等待异步完成:API调用是异步的,触发点击后不能立刻断言,需要等待Promise执行完毕(用
await Promise.resolve()或专门的异步工具函数),再调用wrapper.update()更新组件视图。 - 模拟输入值:如果组件中的
item和price是用户输入的值,测试时需要先模拟输入,确保getItemList的入参符合预期,比如:// 模拟输入框赋值 wrapper.find('[data-testid="item-input"]').simulate('change', { target: { value: 'test-item' } }); wrapper.find('[data-testid="price-input"]').simulate('change', { target: { value: '100' } });
3. 补充验证逻辑
除了表格渲染,还可以添加这些断言来覆盖更多场景:
- 验证loading状态在点击后被设置为
true(如果组件有相关UI)。 - 验证
itemData状态是否正确更新,比如通过wrapper.state('itemData')(Enzyme类组件)或用useState的测试钩子(函数组件需要额外处理)。 - 验证错误分支:模拟API返回失败,检查错误处理逻辑是否执行。
内容的提问来源于stack exchange,提问作者Yogesh
相关产品推荐
相关产品推荐

