基于Vue源码中e2e测试实践

简介: 最近半年博主一直在抽空自研一款React组件库Concis,对于测试原本只支持于jest+enzyme单元测试,而单元测试的缺陷是无法模拟用户的一些操作,如表单组件,只能测试到一些组件渲染mount准确性的测试,对于一系列用户操作的模拟测试是无法做到的,因此博主考虑加入e2e测试从而让组件上线后更加健全。

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

前言

最近半年博主一直在抽空自研一款React组件库Concis,对于测试原本只支持于jest+enzyme单元测试,而单元测试的缺陷是无法模拟用户的一些操作,如表单组件,只能测试到一些组件渲染mount准确性的测试,对于一系列用户操作的模拟测试是无法做到的,因此博主考虑加入e2e测试从而让组件上线后更加健全。

关于单元测试,博主之前整理了一份基于Concis项目自用的总结:

全网最细:Jest+Enzyme测试React组件(包含交互、DOM、样式测试)

技术选型&对Vue的参考

其实博主本身对e2e测试的技术栈比较陌生,于是决定学习一下目前主流框架Vue的e2e测试是如何去做的,学习一下尤大大的技术选型,哈哈哈~

这是github中Vue的test文件夹:

在这里插入图片描述

我们进入到e2e中可以看到,圈起来的都是e2e测试用例,而utils应该就是e2e的初始化用于在每一个测试用例文件执行前初始化的,看一下e2eUtils.ts这个文件:

在这里插入图片描述

好了,技术选型确认完毕,使用的是puppeteer这个包,大概研究一下使用,这里尤大大其实是基于pupeteer一系列page执行性api进行了二次封装并且最终导出,在每一个测试用例中去使用:

在这里插入图片描述
而这些方法博主最后整理了一下,有很详细的备注,具体代码在最后~

Puppeteer测试流程

这里简单介绍一下使用Puppeteere2e测试的整体流程:

  1. 初始化browser实例,构造测试模拟浏览器;
  2. 基于browser实例,新建一个页面实例;
  3. 打开测试url;
  4. 一系列的操作,DOM操作,mock用户;
  5. 关闭页面;
  6. 关闭浏览器;

这里整理了一份入门版简易代码,对照上述流程是这样的:

const testPupeteer = async () => {
   
   
  let brwoser: puppeteer.Browser;
  let page: puppeteer.Page;

  brwoser = await puppeteer.launch({
   
                             //打开浏览器
    headless: false,
  })
  await page.goto("http://localhost:8000");                 //page页面跳转测试url
  await page.type('body .testDiv .username', 'username');   //输入用户名
  await page.type('body .testDiv .password', '123456');     //输入密码
  await page.click('body .testdiv .login-btn');             //登录
}

可以看到,代码很清晰,由于是真实操作,可能有的操作会涉及网络请求,因此都是异步访问按顺序阻塞执行操作。

代码中模拟了一次用户打开浏览器、打开页面、输入用户名密码、点击登录按钮的一次登录操作,但是pupeteer只提供了模拟操作,并没有对操作进行反馈做判断,这时就需要配合jest的断言来进行e2e整体测试了。

在Concis中的实践

这里博主借鉴了Vue的e2e测试方法,即同样的写了一个setup方法用于初始化pupeteer的起步动作,并且二次封装了一系列方法。

e2eUtils.ts代码如下:

import puppeteer from 'puppeteer';

const getExampleUrl = (componentName: string) => {
   
   
  //获取测试组件的页面url
  return `http://localhost:8000/#/common/${
     
     componentName}`;
};
const e2eTestTimeout = 30 * 1000;

const setupPuppeteer = () => {
   
   
  let browser: puppeteer.Browser;
  let page: puppeteer.Page;

  beforeAll(async () => {
   
   
    browser = await puppeteer.launch({
   
   
      headless: false,
    });
  });
  beforeEach(async () => {
   
   
    page = await browser.newPage();

    await page.evaluateOnNewDocument(() => {
   
   
      localStorage.clear();
    });
    page.on('console', (e) => {
   
   
      if (e.type() === 'error') {
   
   
        const err = e.args()[0];
        console.error(`Error from Puppeteer-loaded page:\n`, err);
      }
    });
  });
  afterEach(async () => {
   
   
    await page.close();
  });
  afterAll(async () => {
   
   
    await browser.close();
  });

  const click = async (dom: string, options?: puppeteer.ClickOptions) => {
   
   
    //点击元素
    await page.click(dom, options);
  };
  const getCount = async (dom: string) => {
   
   
    //获取元素数量
    return (await page.$$(dom)).length;
  };
  const getText = async (dom: string) => {
   
   
    //获取元素文本内容
    return await page.$eval(dom, (node) => node.textContent);
  };
  const getValue = async (dom: string) => {
   
   
    //获取文本框的内容
    return await page.$eval(dom, (node) => (node as HTMLInputElement).value);
  };
  const getHtml = async (dom: string) => {
   
   
    //获取元素的innerHTML
    return await page.$eval(dom, (node) => node.innerHTML);
  };
  const getClassList = async (dom: string) => {
   
   
    //获取元素所有类名
    return await page.$eval(dom, (node) => [...node.classList]);
  };
  const getChildrenCount = async (dom: string) => {
   
   
    //获取子元素数量
    return await page.$eval(dom, (node) => node.children.length);
  };
  const domIsShow = async (dom: string) => {
   
   
    //判断元素是否在document中
    const display = await page.$eval(dom, (node) => {
   
   
      return window.getComputedStyle(node).display;
    });
    return display !== 'none';
  };
  const isChecked = async (dom: string) => {
   
   
    //判断多选框是否被选中
    return await page.$eval(dom, (node) => (node as HTMLInputElement).checked);
  };
  const isFocused = async (dom: string) => {
   
   
    //判断元素是否被聚焦
    return await page.$eval(dom, (node) => node === document.activeElement);
  };
  const setValue = async (dom: string, value: string) => {
   
   
    //设置输入框内容
    const el = (await page.$(dom))!;
    await el.evaluate((node) => ((node as HTMLInputElement).value = ''));
    await el.type(value);
  };
  const setText = async (dom: string, value: string) => {
   
   
    //设置元素内容
    const el = (await page.$(dom))!;
    await el.evaluate((node) => ((node as HTMLElement).innerText = ''));
    await el.evaluate((node) => ((node as HTMLElement).innerText = value));
  };
  const enterValue = async (dom: string, value: string) => {
   
   
    //设置输入框内容后回车
    const el = (await page.$(dom))!;
    await el.evaluate((node) => ((node as HTMLInputElement).value = ''));
    await el.type(value);
    await el.press('Enter');
  };
  const clearValue = async (dom: string) => {
   
   
    //清空输入框内容
    await page.$eval(dom, (node) => ((node as HTMLInputElement).value = ''));
  };
  const timeout = async (time: number) => {
   
   
    //延时
    return page.evaluate((time) => {
   
   
      return new Promise((r) => {
   
   
        setTimeout(r, time);
      });
    }, time);
  };
  return {
   
   
    page: () => page,
    click,
    getCount,
    getText,
    getValue,
    getHtml,
    getChildrenCount,
    getClassList,
    domIsShow,
    isChecked,
    isFocused,
    setValue,
    setText,
    enterValue,
    clearValue,
    timeout,
  };
};

export {
   
    getExampleUrl, setupPuppeteer, e2eTestTimeout };

尤大大的源码路径是这个:https://github.com/vuejs/vue/blob/main/test/e2e/e2eUtils.ts
以上代码主要做了这些事情:

  1. getExampleUrl函数用于获取测试的url地址,博主是测试组件库,因此就是每次测试单个组件文档的页面地址;
  2. setupPupeteer函数用于初始化测试环境(浏览器、打开页面)并且封装了一系列方法导出;

接下来看一下博主对于Form组件的测试:

import {
   
    getExampleUrl, setupPuppeteer, e2eTestTimeout } from '../e2eUtils';

describe('form e2e test', () => {
   
   
  const {
   
    click, page, getCount, getText, getChildrenCount, setValue, getValue } = setupPuppeteer();

  const formTest = async () => {
   
   
    await page().goto(getExampleUrl('form'), {
   
   
      waitUntil: 'domcontentloaded',
    });
    //基本demo展示Dom测试
    expect(await getCount('#form-index1 .concis-form .concis-form-item')).toBe(4);
    expect(
      await getText(
        '#form-index1 .concis-form .concis-form-item:nth-child(1) .concis-form-item-label',
      ),
    ).toBe('Username');
    expect(
      await getText(
        '#form-index1 .concis-form .concis-form-item:nth-child(2) .concis-form-item-label',
      ),
    ).toBe('Post');
    expect(
      await getText(
        '#form-index1 .concis-form .concis-form-item:nth-child(3) .concis-form-item-label',
      ),
    ).toBe('');
    expect(
      await getText(
        '#form-index1 .concis-form .concis-form-item:nth-child(4) .concis-form-item-label',
      ),
    ).toBe('');
    //切换布局radioGroup测试
    expect(await getCount('#form-index2 .concis-radio-group')).toBe(1);
    //全局禁用测试
    expect(await getCount('#form-index5 .disabled')).toBe(1);
    //单行禁用测试
    expect(
      await getCount(
        '#form-index7 .concis-form .concis-form-item:nth-child(2) .concis-form-item-disabled',
      ),
    ).toBe(1);
    //测试添加校验label icon显示
    expect(
      await getChildrenCount(
        '#form-index8 .concis-form .concis-form-item:nth-child(1) .concis-form-item-label',
      ),
    ).toBe(1);
    //校验失败的测试
    await click(
      '#form-index8 .concis-form .concis-form-item:nth-child(3) .concis-form-item-content',
    );
    expect(await getText('#form-index8 .concis-form .show-rule-label')).toBe('必须包含a');
    //提交后弹窗测试
    await click(
      '#form-index9 .concis-form .concis-form-item:nth-child(3) .concis-form-item-content',
    );
    expect(await getChildrenCount('.all-container')).toBe(1);
    //测试重置
    await setValue('#form-index6 .concis-form .concis-form-item:nth-child(1) input', '123');
    expect(await getValue('#form-index6 .concis-form .concis-form-item:nth-child(1) input')).toBe(
      '123',
    );
    await click(
      '#form-index6 .concis-form .concis-form-item:nth-child(8) .concis-form-item-content .concis-button:nth-child(2)',
    );
    expect(await getValue('#form-index6 .concis-form .concis-form-item:nth-child(1) input')).toBe(
      '',
    );
  };
  test('test e2e test', async () => await formTest(), e2eTestTimeout);
});

其实有了e2eUtils.ts后,编写测试用例方便了很多,只需要在调用封住好的api基础上加入jest的一些断言,就可以很好的测试组件的交互性。
这里Form的测试都有注释,博主不再依次阐述。

项目目录整理

由于之前只有单元测试,因此需要整理项目目录,整理后的目录如下:

在这里插入图片描述

对于package.json也是可以重新配置:

 "scripts": {
   
   
    "build": "rollup -c ./rollup.config.js",
    "test:unit": "jest ./__tests__/unit",        //单元测试
    "test:e2e": "jest ./__tests__/e2e"            //e2e测试
  },

Concis组件库

博主自研Concis组件库已经有半年时间,组件库也是慢慢成型,目前已整合前端组件30+、组件unit/e2e测试、组件库文档、全局配置等等。

在这里插入图片描述

一路人也是就自己一个人,也是真的挺花费功夫的...博主也是需要更多的有兴趣小伙伴可以关注一下参与进来,一起贡献开源项目,建设一个基于Concis的技术社区,相互讨论、学习、帮助,这是博主从最初的兴趣想去实现一个小项目到目前为止一个更大的愿景。

index.bg.jpg

更具体的可以参考 React组件库Concis,寻求社区中有兴趣的小伙伴加入...

Concis组件库线上链接:http://react-view-ui.com:92
github:https://github.com/fengxinhhh/Concis
npm:https://www.npmjs.com/package/concis

开源不易,欢迎学习和体验,喜欢请多多支持,有问题请留言,如果此文对你有帮助,博主需要你的支持,感谢。

目录
相关文章
|
5月前
|
人工智能 自然语言处理 测试技术
从人工到AI驱动:天猫测试全流程自动化变革实践
天猫技术质量团队探索AI在测试全流程的落地应用,覆盖需求解析、用例生成、数据构造、执行验证等核心环节。通过AI+自然语言驱动,实现测试自动化、可溯化与可管理化,在用例生成、数据构造和执行校验中显著提效,推动测试体系从人工迈向AI全流程自动化,提升效率40%以上,用例覆盖超70%,并构建行业级知识资产沉淀平台。
从人工到AI驱动:天猫测试全流程自动化变革实践
|
5月前
|
数据采集 存储 人工智能
从0到1:天猫AI测试用例生成的实践与突破
本文系统阐述了天猫技术团队在AI赋能测试领域的深度实践与探索,讲述了智能测试用例生成的落地路径。
从0到1:天猫AI测试用例生成的实践与突破
|
6月前
|
Java 测试技术 API
自动化测试工具集成及实践
自动化测试用例的覆盖度及关键点最佳实践、自动化测试工具、集成方法、自动化脚本编写等(兼容多语言(Java、Python、Go、C++、C#等)、多框架(Spring、React、Vue等))
335 6
|
6月前
|
人工智能 边缘计算 搜索推荐
AI产品测试学习路径全解析:从业务场景到代码实践
本文深入解析AI测试的核心技能与学习路径,涵盖业务理解、模型指标计算与性能测试三大阶段,助力掌握分类、推荐系统、计算机视觉等多场景测试方法,提升AI产品质量保障能力。
|
6月前
|
人工智能 自然语言处理 测试技术
AI测试平台的用例管理实践:写得清晰,管得高效,执行更智能
在测试过程中,用例分散、步骤模糊、回归测试效率低等问题常困扰团队。霍格沃兹测试开发学社推出的AI测试平台,打通“用例编写—集中管理—智能执行”全流程,提升测试效率与覆盖率。平台支持标准化用例编写、统一管理操作及智能执行,助力测试团队高效协作,释放更多精力优化测试策略。目前平台已开放内测,欢迎试用体验!
|
7月前
|
人工智能 资源调度 jenkins
精准化回归测试:大厂实践与技术落地解析
在高频迭代时代,全量回归测试成本高、效率低,常导致关键 bug 漏测。精准化测试通过代码变更影响分析,智能筛选高价值用例,显著提升测试效率与缺陷捕获率,实现降本增效。已被阿里、京东、腾讯等大厂成功落地,成为质量保障的新趋势。
|
7月前
|
搜索推荐 Devops 测试技术
避免无效回归!基于MCP协议的精准测试影响分析实践
本文揭示传统测试的"孤岛困境",提出MCP(Model Context Protocol)测试新范式,通过模型抽象业务、上下文感知环境和协议规范协作,实现从机械执行到智能测试的转变。剖析MCP如何颠覆测试流程,展示典型应用场景,并提供团队落地实践路径,助力测试工程师把握质量效率革命的新机遇。
|
7月前
|
人工智能 缓存 自然语言处理
大模型性能测试完全指南:从原理到实践
本文介绍了大模型性能测试的核心价值与方法,涵盖流式响应机制、PD分离架构、五大关键指标(如首Token延迟、吐字率等),并通过实战演示如何使用Locust进行压力测试。同时探讨了多模态测试的挑战与优化方向,帮助测试工程师成长为AI系统性能的“诊断专家”。
|
9月前
|
人工智能 Java 测试技术
SpringBoot 测试实践:单元测试与集成测试
在 Spring Boot 测试中,@MockBean 用于创建完全模拟的 Bean,替代真实对象行为;而 @SpyBean 则用于部分模拟,保留未指定方法的真实实现。两者结合 Mockito 可灵活控制依赖行为,提升测试覆盖率。合理使用 @ContextConfiguration 和避免滥用 @SpringBootTest 可优化测试上下文加载速度,提高测试效率。
440 5
|
10月前
|
缓存 测试技术 API
RESTful接口设计与测试实践
通过理解和实践上述原则和步骤,你就可以设计和测试你的RESTful接口了。最后,它可能会变成你为优化系统性能和用户体验所使用的重要工具,因为好的接口设计可以使得从服务器端到客户端的通信更加直接和有效,同时提升产品的使用体验和满意度。如此一来,写一个好的RESTful接口就变成一种享受。
321 18