头图
由于图片和格式解析问题,可前往 阅读原文

前端自动化测试在提高代码质量、减少错误、提高团队协作和加速交付流程方面发挥着重要作用。它是现代软件开发中不可或缺的一部分,可以帮助开发团队构建可靠、高质量的应用程序

单元测试(Unit Testing)和端到端测试(End-to-End Testing)是两种常见的测试方法,它们在测试的范围、目的和执行方式上有所不同。单元测试和端到端测试不是相互排斥的,而是互补的。它们在不同的层面和阶段提供了不同的价值,共同构成了一个全面的测试策略

单测和端测区别

单元测试(Unit)

  • 单元测试关注于最小的可测试单元,如函数、方法或模块
  • 目的是验证代码中的每个独立单元(如函数)是否按照预期工作
  • 通常是自动化的、快速执行的,且不依赖于外部资源或其他模块
  • 验证单个代码单元的行为,提供快速反馈,并帮助捕获和修复问题

端到端测试(End-to-End)

  • 从用户角度出发,测试整个应用程序的功能和流程
  • 模拟真实的用户交互和场景,从应用程序的外部进行测试。跨多个模块、组件和服务进行,以确保整个应用程序的各个部分正常协同工作
  • 涉及用户界面(UI)交互、网络请求、数据库操作等,以验证整个应用程序的功能和可用性

总之,单元测试主要关注代码内部的正确性,而端到端测试关注整体功能和用户体验。结合使用这两种测试方法可以提高软件的质量和可靠性。在项目中尤其是公共依赖如组件库至少都需要单测,端测相对来说比较繁琐点,但是也是程序稳定的重要验证渠道

Cypress

这里选择Cypress作为端测框架,Cypress 是一个用于前端端到端(End-to-End,E2E)测试的开源测试框架。它被设计用于对 Web 应用程序进行自动化测试,可以模拟用户与应用程序进行交互的行为,并验证应用程序的功能和用户体验

安装使用

你可以选择自己的npm包管理器进行安装

➜ npm install cypress -D

配置npm脚本打开cypress

{
    "scripts": {
        "test:e2e": "cypress open"
    }
}

或者直接使用./node_modules/.bin/cypress open。启动后出弹出cypress的界面,可以进行端到端测试和组件测试。从下图界面可以看出不管是端测还是组件测试都没有配置(Not Configured),这里我们选择第一个端到端测试:

进去后由于是初次使用cypress会引导对项目做初始化配置,点击Continue会在项目中生成对应的文件:

端到端测试cypress会检测本地计算机安装的浏览器,你可以选择要用哪个浏览器进行测试,或者使用electron。这里我选择使用electron

进来后就到了测试用例界面,初次下载不会有任何测试用例的。界面会引导你去创建第一个测试用例,左侧用来生成官方的测试例子,其可以作为用例的参考;右侧自己来创建测试用例,初次使用我们先点击生成官方例子:

下图便是官方的测试例子,可以看到有很多,对应的文件路径在cypress/e2e

这些单测例子中随便点击一个就会测试对应的用例,这里我们点击todo.cy.js就可以开始测试了

可以看到测试界面主要包含两大板块,左侧主要是测试日志、统计、快照之类,右侧便是web界面。cypress会根据测试用例模拟用户行为,并生成对应的日志;web界面会显示每个步骤的形态,就好像是用户在操作一样


<div class='img-title'>动图演示</div>

除了使用open打开对应的界面外,也可以使用终端进行测试不用打开界面,只需要执行cypress run命令即可

配置

cypress配置包括全局、端测、组件测试几个方面,可以通过cypress.config.js配置文件进行配置:

const { resolve } = require("path");
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  // 端测配置
  e2e: {},
  // 组件测试配置
  component: {},
  /* 其他的全局配置 */
});

以下是一些常见的配置:

  • baseUrl:端测访问的基本地址,比如测试本地的web服务http://localhost:8080,这样在测试用例中就可以使用相对路径了
  • specPattern:测试用例文件,包含单测和组件测试,支持glob形式。这样就可以自定义测试用例的位置了
  • fixturesFolder:用来存放测试mock数据,默认读取根路径下的cypress/fixtures路径下的文件,你可以通过此属性修改对应的测试数据路径
  • supportFile:自定义全局配置、功能函数、自定义命令、环境变量等等,在每个测试文件执行前都会先加载执行全局的配置文件。默认的路径为cypress/support/**,可以自行修改,如果没有全局的配置请设置成false
  • experimentalRunAllSpecs:值为true时测试界面会有一个测试全部用例的按钮,默认会false
  • video:当执行cypress run进行测试后生成对应的视频文件,设置成false不会生成
  • screenshotsFolder:快照保存路径,默认路径cypress/screenshots
  • videosFolder:视频保存路径,默认路径cypress/videos
  • screenshotOnRunFailure:当cypress run失败时生成对应的快照
  • downloadsFolder:当测试用例中下载文件时保存的路径
  • viewportWidth / viewportHeight:设置界面视口大小,可以模拟不同尺寸的屏幕

有的配置对于 e2e和Component 都有,可以在全局配置,也可以在不同类型的测试中进行覆盖,基本配置就说这么多,更多配置参考 官方文档

大概的配置结构如下:

<root>
├── cypress.config.js # 配置文件
├── cypress
│     ├── fixtures # 测试数据
│   │     └── user.json
│     └── supports # 全局配置
│         └── index.js

基本端测

这里我们尝试使用端测来测试Vue3的组件,端测我们要提供页面真实的模拟用户的交互等等,所以需要一个web服务应用,内部使用我们待测试的vue组件,然后模拟用户行为

  1. 搭建web服务,假设这里使用vite搭建一个web应用
  2. 配置cypress的单测baseUrl,这样在用例中就可以使用相对地址了

    module.exports = defineConfig({
      e2e: {
        baseUrl: "http://localhost:10020", // 配置web基本地址
        video: false, # 不生成视频
        screenshotOnRunFailure: false, # 不生成快照
        supportFile: false, # 不设置全局配置
        specPattern: "src/**/*.e2e.(tsx|ts)", # 设置端测用例的位置
      },
    });
  3. 新建button组件的测试用例:

    // src/button/__tests__/button.e2e.tsx
    /// <reference types="cypress" />
    describe("首页测试", () => {
      it("测试外观", () => {
        cy.visit("/button");
        cy.get(".i-btn").contains("按钮").trigger("click");
      });
    });
  4. 启动测试:npm run test:e2e
  5. 可以看到我们的界面已经有了刚刚新建的测试用例文件了

  1. 点击测试用例开始测试:


<div class='img-title'>动图演示</div>

因为比较简单执行的比较快。左侧的测试日志鼠标移到对应的位置会显示对应的快照

以上便是一个最简单上手的端测用例,比较简单相应你已经也成功了。其他更多的测试用例api这里不做过多介绍,其实和jest差不多,你可以通过 官方文档 了解更多

代码覆盖率

cypress也支持代码覆盖率的报告,通过安装对应的插件就可以轻松搞定

➜ npm i @cypress/code-coverage babel-plugin-istanbul -D

添加babel配置

// .babelrc
{
  "plugins": ["istanbul"]
}

添加全局功能配置:

// cypress/support/index.js
import '@cypress/code-coverage/support';

对cypress进行配置:

// cypress.config.ts
const { resolve } = require("path");
const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {
      require('@cypress/code-coverage/task')(on, config);
      on('file:preprocessor', require('@cypress/code-coverage/use-babelrc'));

      return config;
    },
    supportFile: "cypress/supports/index.js",
  },
});

更多请参考对应的 官方文档

钩子函数

在 Cypress 测试框架中,有一些钩子函数可以用于在测试执行的不同阶段执行额外的操作或设置。这些钩子函数是 Cypress 提供的全局函数,可以在测试文件中使用

  • before: 函数在每个测试套件(describe 块)运行之前执行,并且只会执行一次。它可以用于设置测试套件级别的准备工作,例如初始化测试数据、登录用户等
  • after: 函数在每个测试套件(describe 块)运行完毕之后执行,并且只会执行一次。它可以用于清理测试套件级别的资源,例如清除测试数据、退出登录等
  • beforeEach: 函数在每个测试用例(it 块)运行之前执行。它通常用于设置每个测试用例的前置条件,例如重置状态、模拟请求等
  • afterEach: 函数在每个测试用例(it 块)运行完毕之后执行。它通常用于清理每个测试用例的后续操作,例如清除临时数据、还原状态等

常见问题

项目中可能会使用Jest进行单元测试、cypress进行端到端测试,由于jest和cypress有着形同的api,如:describe、it等等,会出现类型冲突的错误。这里我是在tsconfig中屏蔽掉cypress测试文件就可以了

{
  "compilerOptions": {},
  "exclude": ["src/**/*.e2e.tsx"] // 屏蔽掉cypress测试用例
}

测试前端组件

这里我们先定义一个LazyComponent组件,主要就是来懒加载目标组件,根据网络情况呈现最终的显示结果:

// 参考代码
export const LazyComponent = function (opts: ILazyComponent) {
  let delay = 300; // 默认300ms
  let LoadingComponent = LazyLoading;
  let errorComponent = LazyError;
  let loader: () => Promise<any>;
  let requiredCache = true;

  if (isFunction(opts)) {
    loader = opts;
  } else {
    loader = opts.loader;
    LoadingComponent = opts.loadingComponent || LoadingComponent;
    errorComponent = opts.errorComponent || errorComponent;
    delay = opts.delay ?? delay;
    requiredCache = opts.cache ?? requiredCache;
  }

  const isSpin = isFunction(opts) ? true : opts?.isSpin ?? true;
  const minSkeleton = isFunction(opts) ? 2 : opts?.minSkeleton ?? 2;
  const withCard = isFunction(opts) ? true : opts?.withCard ?? true;

  return defineComponent({
    render() {
      const Component: any = defineAsyncComponent({
        loader: () =>
          new Promise(async (resolve, reject) => {
            try {
              const cacheKey = loader?.toString();
              const isCached = lazyComponentsCache.has(cacheKey);
              if (isCached) return resolve(lazyComponentsCache.get(cacheKey));
              await sleep(delay);
              const res = await loader();
              requiredCache && lazyComponentsCache.set(cacheKey, res);
              if (lazyComponentsCache.size > MAX_CACHE_SIZE) {
                lazyComponentsCache.delete(lazyComponentsCache.keys().next().value);
              }
              resolve(res);
            } catch (err) {
              console.error("【LazyComponent Fail】", err);
              reject(err);
            }
          }),
        loadingComponent: () => (
          <LoadingComponent
            minSkeleton={minSkeleton}
            withCard={withCard}
            isSpin={isSpin}
          />
        ),
        errorComponent,
        delay: 0,
      });
      return <Component />;
    },
  });
};

然后我们创建一个组件测试用例文件lazy-component.cy.tsx,然后根据不同的条件去渲染预期的页面效果

import { CustomLoading } from "~/client/components/common/loading";
import { LazyComponent } from "~/client/layout/page/lazy-component";
import AnalysisPage from "~/client/views/dashboard/analysis";

describe("测试组件 LazyComponent", () => {
  it("LazyComponent 默认卡片加载", () => {
    cy.mount(
      LazyComponent({
        loader: () => Promise.resolve(<span>我是最终组件1</span>),
        withCard: true,
        isSpin: false,
        minSkeleton: 5,
        delay: 3000,
      })
    );
    cy.wait(4000);
  });

  it("LazyComponent 默认Spin加载", () => {
    cy.mount(
      LazyComponent({
        loader: () => Promise.resolve(<span>我是最终组件2</span>),
        withCard: false,
        isSpin: true,
        delay: 3000,
      })
    );
    cy.wait(5000);
  });

  it("LazyComponent 测试缓存(当前组件不会loading加载,直接立即显示)", () => {
    cy.mount(
      LazyComponent({
        loader: () => Promise.resolve(<span>我是最终组件2</span>),
        withCard: true,
        delay: 3000,
      })
    );
    cy.wait(5000);
  });

  it("LazyComponent成功", () => {
    cy.mount(
      LazyComponent({
        loader: () => Promise.resolve(<AnalysisPage />),
        loadingComponent: () => (
          <CustomLoading>
            <strong>Loading...</strong>
          </CustomLoading>
        ),
        delay: 3000,
      })
    );
    cy.wait(5000);
  });

  it("LazyComponent失败", () => {
    cy.mount(
      LazyComponent({
        loader: () => Promise.resolve(<h3>出错了...</h3>),
        loadingComponent: () => <div style={{ color: "red", "font-size": "24px", textAlign: "center" }}>请稍后...</div>,
        delay: 2000,
      })
    );
  });
});

测试流程概览:

参考文档

由于图片和格式解析问题,可前往 阅读原文

大卫talk
74 声望9 粉丝

攻粽号:码上来財(mslaicai)


« 上一篇
Jest单元测试