首页
/ Cypress 组件测试实战:在 @cypress/vue 中 Stub axios 默认导出的完整方法

Cypress 组件测试实战:在 @cypress/vue 中 Stub axios 默认导出的完整方法

2026-09-07 16:26:26作者:范靓好Udolf

本文基于 Cypress 官方仓库中 npm/vue 包的 advanced/mocking-axios 示例文档,讲解 Vue 组件测试中如何对 node_modules 里的 axios 进行方法级打桩:为什么必须 stub 与 spec 文件相同的模块实例、axios.get 的 stub 写法与断言方式,以及如何结合 JSON fixture 文件构造更真实的 mock 响应。读完你可以掌握一套可直接复制到项目中运行的 axios mock 组件测试方案。

Cypress Runner 中两个 axios.get mock 测试通过,右侧组件视口渲染出 101 First User / 102 Second User 两条列表

场景背景:axios 是真实依赖,不能依赖网络

@cypress/vue(Cypress 官方 Vue 组件测试挂载库)的组件测试中,组件代码和测试代码运行在同一个浏览器 bundle 上下文里。示例中的 Users.vue 直接 import axios from 'axios',在 created 钩子中请求远端接口:

// Users.vue(节选)
import axios from 'axios'

export default {
  data () {
    return {
      users: [],
    }
  },

  // Fetches posts when the component is created.
  created () {
    axios.get('https://jsonplaceholder.cypress.io/users?_limit=3')
    .then((response) => {
      // JSON responses are automatically parsed.
      this.users = response.data
    })
  },
}

这个组件的渲染完全依赖 axios.get 的返回值:users 有数据时模板才渲染 <ul> 列表。如果组件测试中不去拦截这个请求,测试就会不稳定地依赖外部 API。原始文档(README.md)对此的定位很明确:Axios 安装在 node_modules 中,并被 Vue 组件直接使用,因此正确做法不是 mock 网络层,而是对模块的方法进行 stub。

核心思路:stub 默认导出上的方法

原始文档给出的关键结论是:

HTTP helpers live on the default export. Import the default and call axios.get, then stub that method on the same module instance your spec imports.

也就是说,axios 的 get/post 等 HTTP 辅助方法挂在默认导出对象上。组件里写 import axios from 'axios' 然后 axios.get(...);那么 spec 里也必须 import Axios from 'axios'(导入同一个默认导出),再在这个默认导出上 stub get

这里“same module instance”是成败关键:在组件测试中,spec 文件和被挂载的组件由同一个 dev server 打包进同一个运行环境,模块会被 bundler 的模块缓存共享——spec 导入的 Axios 与组件里使用的 axios 是同一对象,因此在其中一个引用上打桩,另一处的调用自然命中桩。反过来,如果组件通过具名导入(如 import { get } from 'axios')或自行创建实例(axios.create()),默认导出上的 stub 就无法覆盖,需要另行处理。从示例代码结构看,本示例选择的就是默认导出 + 方法打桩这一条路径,这也是 axios 最常见的用法。

stub 的基本形态(来自原文档):

// component code
import axios from 'axios'
axios.get('...').then(...)

// spec file: import the default and stub .get
import Axios from 'axios'

cy.stub(Axios, 'get').resolves({
  data: [
    {
      id: 101,
      name: 'Test User',
    },
  ],
})

注意 resolves 的值是完整的 axios 响应对象:组件里读取的是 response.data,所以 mock 时必须包一层 data,而不是直接把数组传进去——否则组件中 this.users = response.data 拿到 undefined,断言必然失败。

完整 spec 示例:两种 mock 数据源

仓库中的 Users.cy.js 给出了两个完整测试用例,完整代码如下:

/// <reference types="cypress" />
import { mount } from '@cypress/vue'
import Users from './Users.vue'
// we can load list of mock users straight from JSON file
import mockUsers from './user.list.json'

import Axios from 'axios'

describe('Mocking axios.get with default import', () => {
  it('renders mocked data', () => {
    cy.stub(Axios, 'get')
    .resolves({
      data: [
        {
          id: 101,
          name: 'Test User',
        },
      ],
    })
    .as('get')

    mount(Users)
    // mock response is used
    cy.get('li').should('have.length', 1)
    cy.get('@get').should('have.been.calledOnce')
  })

  it('stubs with JSON loaded from fixture file', () => {
    cy.stub(Axios, 'get')
    .resolves({
      data: mockUsers,
    })
    .as('get')

    mount(Users)
    // mock response is used
    cy.get('li').should('have.length', 2)
    cy.get('@get').should('have.been.calledOnce')
  })
})

两个用例演示了两种数据源与两类断言:

  1. 内联 mock 数据:直接在 resolves 里写死响应,断言 li 长度为 1;
  2. 从 JSON fixture 文件加载import mockUsers from './user.list.json',mock 数据放在 user.list.json 中(内容为两条用户:101 First User102 Second User),断言 li 长度为 2。把数据抽到独立 JSON 文件的价值在于:测试数据与断言逻辑分离,便于多人维护与复用;
  3. 调用次数断言:两个用例都用 .as('get') 给 stub 起别名,再用 cy.get('@get').should('have.been.calledOnce') 验证组件确实发起且仅发起了一次 axios.get。这同时验证了「组件走的是桩,而不是真实网络」。

运行配置:在 @cypress/vue 项目中跑起来

该示例位于 npm/vue 包内,其组件测试由 npm/vue/cypress.config.ts 驱动,关键配置如下:

import { defineConfig } from 'cypress'

export default defineConfig({
  'viewportWidth': 500,
  'viewportHeight': 500,
  'responseTimeout': 2500,
  'projectId': '134ej7',
  'e2e': {
    'supportFile': false,
  },
  'component': {
    experimentalSingleTabRunMode: true,
    excludeSpecPattern: 'examples/**/*',
    devServer: {
      bundler: 'vite',
      framework: 'vue',
    },
  },
})

要点:

  • component.devServer 指定 bundler: 'vite'framework: 'vue'——正是这个 dev server 把 spec 与组件打进同一个 bundle,从而保证 spec 里的 Axios 与组件里的 axios 是同一模块实例,stub 才能生效;
  • viewportWidth/Height 为 500x500,与上面截图中组件视口尺寸一致;
  • 测试 spec 采用 .cy.js 命名(Users.cy.js),位于 cypress/component/ 下,符合 Cypress 组件测试的默认 spec 查找规则。

依赖方面,npm/vue/package.jsonaxios^1.15.1)与 vue3.2.47)均为 devDependencies,说明示例里的 axios 是测试项目本地安装的普通 npm 依赖。运行命令也定义在该文件里:

# 在 npm/vue 目录下
yarn cy:run    # 等价于 node ../../scripts/cypress.js run --component --project ${PWD}
yarn cy:open   # 打开 Runner 交互查看

与同类 mock 手段的边界

同一目录 cypress/component/advanced/ 下还有 mocking-fetchmocking-imports 等相邻示例,可以据此判断本文方案的适用边界:

  • 当请求走的是 axios 这类独立 npm 模块时,用本文的方法:stub 模块默认导出上的方法,粒度最细,且能断言调用次数与参数;
  • 当请求走的是浏览器原生 fetch 时,由于 fetch 挂在 window 上,可以直接 mock window.fetch,见 mocking-fetch 示例
  • 当组件依赖的是本地模块/组件而非网络库时,则属于 mocking-importsmocking-components 覆盖的范畴。

小结

对「组件直接 import axios」的 Vue 项目,Cypress 组件测试的标准打法可以归纳为三步:

  1. spec 中 import Axios from 'axios',保证拿到与组件相同的模块实例(依赖 vite/webpack 组件测试 dev server 的模块共享);
  2. cy.stub(Axios, 'get').resolves({ data: ... }),注意 mock 的是完整响应对象,data 字段不可省略,并可用 .as() 登记别名;
  3. 用渲染断言(li 数量)验证数据确实来自桩,用 have.been.calledOnce 验证组件只发起一次预期请求;mock 数据可内联,也可抽到 JSON fixture 文件(如 user.list.json)便于维护。

这套做法让组件测试完全不触碰真实网络,同时保留了 axios 响应的真实数据结构,是 @cypress/vue 组件测试中处理 HTTP 依赖的推荐姿势。

登录后查看全文
热门项目推荐
相关项目推荐