You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 19:20:30