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

如何为使用readQuery/writeQuery的ApolloClient缓存更新编写单元测试

如何测试Apollo Client中useMutation的自定义update缓存逻辑

优先选择直接测试customUpdateFunction的方案:该函数本身和React上下文完全解耦,仅依赖传入的缓存实例与mutation返回数据,不需要渲染组件就能覆盖所有逻辑分支,测试运行速度快、问题定位精准。如果需要验证组件和hook的完整联动,再补充组件层面的集成测试即可。


方案1:直接对customUpdateFunction做单元测试(推荐)

测试核心思路是用Apollo官方提供的真实InMemoryCache实例作为入参,不要手动mock缓存读写方法,才能真实复现readQuery/writeQuery的实际行为,覆盖两个核心场景:

  • 缓存中不存在LIST_QUERY对应数据时,函数直接返回,不执行写入操作、不抛错
  • 缓存中存在LIST_QUERY对应数据时,函数正确执行不可变更新,移除目标条目后写回缓存

参考测试代码:

import { InMemoryCache } from '@apollo/client';
import { LIST_QUERY } from './your-gql-defs';
// 注意要把customUpdateFunction从组件文件中导出,方便测试引入
import { customUpdateFunction } from './ExampleComponent';

describe('customUpdateFunction', () => {
  let testCache;
  // 测试用初始列表数据
  const mockInitialList = {
    itemList: [
      { __typename: 'Item', id: 'i1', content: '测试条目1' },
      { __typename: 'Item', id: 'i2', content: '测试条目2' }
    ]
  };
  // 模拟删除i1后的mutation返回结果
  const mockMutationResult = {
    data: {
      removeItem: { __typename: 'Item', id: 'i1' }
    }
  };

  beforeEach(() => {
    // 每个用例初始化全新缓存,避免状态交叉污染
    testCache = new InMemoryCache();
    // 给缓存方法加spy,验证调用逻辑是否符合预期
    jest.spyOn(testCache, 'readQuery');
    jest.spyOn(testCache, 'writeQuery');
  });

  test('缓存无目标查询数据时直接退出,不执行写入', () => {
    customUpdateFunction(testCache, mockMutationResult);

    expect(testCache.readQuery).toHaveBeenCalledWith({ query: LIST_QUERY });
    expect(testCache.writeQuery).not.toHaveBeenCalled();
  });

  test('缓存存在目标查询数据时,正确移除对应条目后写回缓存', () => {
    // 预先写入初始列表数据到缓存
    testCache.writeQuery({
      query: LIST_QUERY,
      data: mockInitialList
    });

    customUpdateFunction(testCache, mockMutationResult);

    const updatedCacheData = testCache.readQuery({ query: LIST_QUERY });
    // 验证更新结果
    expect(updatedCacheData.itemList).toHaveLength(1);
    expect(updatedCacheData.itemList[0].id).toBe('i2');
    expect(testCache.writeQuery).toHaveBeenCalledTimes(1);
  });
});

方案2:组件层面集成测试(补充验证)

如果需要验证useMutation钩子和update逻辑在组件实际运行时的完整联动,可以用React测试库配合Apollo官方提供的MockedProvider做集成测试:

  • 配置mock数据:分别给LIST_QUERY、REMOVE_ITEM_MUTATION配置对应的mock返回
  • 渲染组件后触发删除操作
  • 通过页面渲染的内容、或者从MockedProvider实例中取出缓存对象,验证更新结果符合预期

参考测试代码:

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { LIST_QUERY, REMOVE_ITEM_MUTATION, VARIABLES, ExampleComponent } from './ExampleComponent';

// 带初始缓存数据的mock配置
const testMocks = [
  {
    request: { query: LIST_QUERY },
    result: {
      data: {
        itemList: [
          { __typename: 'Item', id: 'i1', content: '测试条目1' },
          { __typename: 'Item', id: 'i2', content: '测试条目2' }
        ]
      }
    }
  },
  {
    request: { query: REMOVE_ITEM_MUTATION, variables: VARIABLES },
    result: {
      data: { removeItem: { __typename: 'Item', id: 'i1' } }
    }
  }
];

test('触发删除操作后,页面列表正确移除目标条目', async () => {
  render(
    <MockedProvider mocks={testMocks} addTypename={true}>
      <ExampleComponent />
    </MockedProvider>
  );

  // 触发删除动作,比如点击对应删除按钮
  fireEvent.click(screen.getByRole('button', { name: '删除测试条目1' }));

  await waitFor(() => {
    expect(screen.queryByText('测试条目1')).not.toBeInTheDocument();
    expect(screen.getByText('测试条目2')).toBeInTheDocument();
  });
});

注意:所有测试场景都不要手动mockreadQuery/writeQuery的内部实现,必须使用真实的InMemoryCache实例,否则可能出现mock行为和Apollo实际缓存逻辑不一致,导致测试通过但线上功能异常的问题。

内容的提问来源于stack exchange,提问作者jmancherje

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 18:27:40