基于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

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

目录
相关文章
|
25天前
|
测试技术
软件测试的艺术:探索式测试的实践与思考
在软件开发的广阔海洋中,测试是确保航船稳健行驶的关键。本文将带你领略探索式测试的魅力,一种结合创造性思维和严格方法论的测试方式。我们将一起揭开探索式测试的神秘面纱,了解其核心概念、实施步骤和带来的效益。通过实际代码示例,你将学会如何将探索式测试融入日常的软件质量保证流程中,提升测试效率与质量。
|
15天前
|
数据采集 监控 机器人
浅谈网页端IM技术及相关测试方法实践(包括WebSocket性能测试)
最开始转转的客服系统体系如IM、工单以及机器人等都是使用第三方的产品。但第三方产品对于转转的业务,以及客服的效率等都产生了诸多限制,所以我们决定自研替换第三方系统。下面主要分享一下网页端IM技术及相关测试方法,我们先从了解IM系统和WebSocket开始。
33 4
|
17天前
|
人工智能 JavaScript 前端开发
自动化测试框架的演进与实践###
本文深入探讨了自动化测试框架从诞生至今的发展历程,重点分析了当前主流框架的优势与局限性,并结合实际案例,阐述了如何根据项目需求选择合适的自动化测试策略。文章还展望了未来自动化测试领域的技术趋势,为读者提供了宝贵的实践经验和前瞻性思考。 ###
|
15天前
|
测试技术 Python
探索软件测试的深度与广度:从理论到实践
在数字化时代,软件已成为我们生活中不可或缺的一部分。随着技术的不断进步和用户需求的多样化,确保软件质量变得尤为重要。本文将深入浅出地介绍软件测试的核心概念、类型及其在软件开发生命周期中的重要性。我们将通过实际案例,展示如何实施有效的测试策略,并探讨自动化测试的未来趋势,旨在为读者提供一套完整的软件测试知识体系,帮助提升软件质量和开发效率。
|
16天前
|
测试技术 Python
探索软件测试的奥秘:从理论到实践
在软件开发的宇宙中,软件测试犹如一颗璀璨的星辰,指引着质量的方向。本文将带你穿梭于软件测试的理论与实践之间,揭示其内在的逻辑和魅力。从测试的重要性出发,我们将探讨不同类型的测试方法,并通过实际案例分析,深入理解测试用例的设计和应用。最后,我们将通过一个代码示例,展示如何将理论知识转化为实际操作,确保软件质量的同时,也提升你的测试技能。让我们一起踏上这段探索之旅,发现软件测试的无限可能。
|
19天前
|
jenkins 测试技术 持续交付
自动化测试框架的搭建与实践
在软件开发领域,自动化测试是提升开发效率、确保软件质量的关键手段。本文将引导读者理解自动化测试的重要性,并介绍如何搭建一个基本的自动化测试框架。通过具体示例和步骤,我们将探索如何有效实施自动化测试策略,以实现软件开发流程的优化。
43 7
|
18天前
|
测试技术
探索软件测试的奥秘:从理论到实践
本文深入探讨了软件测试的基本概念、重要性、主要类型以及实施策略。通过分析不同测试阶段和相应的测试方法,文章旨在为读者提供一套完整的软件测试知识体系,帮助他们更好地理解和应用测试技术,确保软件产品的质量和可靠性。
36 4
|
22天前
|
机器学习/深度学习 人工智能 自然语言处理
智能化软件测试:AI驱动的自动化测试策略与实践####
本文深入探讨了人工智能(AI)在软件测试领域的创新应用,通过分析AI技术如何优化测试流程、提升测试效率及质量,阐述了智能化软件测试的核心价值。文章首先概述了传统软件测试面临的挑战,随后详细介绍了AI驱动的自动化测试工具与框架,包括自然语言处理(NLP)、机器学习(ML)算法在缺陷预测、测试用例生成及自动化回归测试中的应用实例。最后,文章展望了智能化软件测试的未来发展趋势,强调了持续学习与适应能力对于保持测试策略有效性的重要性。 ####
|
22天前
|
敏捷开发 Devops 测试技术
探索自动化测试之美:从理论到实践
在软件开发的海洋中,自动化测试犹如一座灯塔,指引着项目向着质量和效率的彼岸。本文将扬帆起航,从自动化测试的意义出发,穿越工具选择的海域,停靠在实战演练的岛屿,最终抵达持续集成的港湾。我们将通过一个具体的代码示例,体验自动化测试的魅力,并分享如何将这些实践应用到日常的软件质量保证过程中。
|
21天前
|
存储 算法 C语言
用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容
本文探讨了用C语言开发游戏的实践过程,包括选择游戏类型、设计游戏框架、实现图形界面、游戏逻辑、调整游戏难度、添加音效音乐、性能优化、测试调试等内容,旨在为开发者提供全面的指导和灵感。
36 2