持续集成之测试篇

前言

随着工程化的发展,人力测试已经很难满足生产需要,尤其是某些团队、开源工作者可能并没有专业的测试人员。以Web领域曾经的王者jQuery为例,几百人协作,几百个版本发布,如果没有自动化的测试手段,怎么能保障每个版本的稳定?

通常来讲,自动化测试分为2种,一种是unit(单元测试),一种是E2E(端到端测试)。前者把代码看成一个个组件,对每个组件进行单独测试,测试内容主要是组件内每一个函数的返回结果是不是和期望值一样,代码覆盖率是指代码中每个函数的每种情况(也称为分支)的测试情况。后者则是在一个真实的浏览器环境中运行整个应用,将程序当作黑盒,对于测试的输入,看能否得到预期得到的结果,验证需求是否正确完成,也适用于代码重构,因开发人员和测试人员的职责不同,编写的侧重点也不同。

单元测试(unit)

单元测试是软件领域必备的能力,也是各种开发框架是否完整自洽的核心功能。编写单元测试是为了验证小的、独立的代码单元是否按预期工作。一个单元测试通常覆盖一个单个函数、类、组合式函数或模块。

一般来说,单元测试将捕获函数的业务逻辑和逻辑正确性的问题,适用于独立的业务逻辑、组件、类、模块或函数,不涉及 UI 渲染、网络请求或其他环境问题。

以前端开发为例,JavaScript代码的运行环境有4种,一是浏览器,二是Node.js,三是Deno,四是Bun。 我们在当年ES5的项目中,曾使用过karma,一个Google开源的基于Node.jsJavaScript 测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到CIContinuous integration)工具,也可和其它代码编辑器一起使用。它的特点是启动一个web服务,将测试代码在浏览器(可配置多种)运行,最终生成测试报告。

而时至今日,你在主流框架(React、Vue、Angular)的官方文档推荐的方案中是找不到它的,原因也很简单,它需要启动浏览器,无论是有界面还是无界面,终究是没有在Node.js或Deno直接测试快捷;随着前端工程化的逐步完善,我们不用再关注应用环境的差异,只需要关注逻辑的正确性即可,而代码中所需的DOM相关的API已经被模拟的很完善了。

测试框架很多,这里只介绍两种:mocha+chai与Jest,功能大同小异,让你有个大概的印象,更多时候,你只需要到所用框架的官网看推荐的工具即可。

mocha+chai

mocha

mochaJavaScript的一种单元测试框架,既可以在浏览器环境下运行,也可以在Node.js环境下运行。

使用mocha,我们就只需要专注于编写单元测试本身,然后,让mocha去自动运行所有的测试,并给出测试结果。

mocha的特点主要有:

  • 既可以测试简单的JavaScript函数,又可以测试异步代码,因为异步是JavaScript的特性之一;
  • 可以自动运行所有测试,也可以只运行特定的测试;
  • 可以支持beforeafterbeforeEachafterEach来编写初始化代码。

describe 表示测试套件,是一序列相关程序的测试;it表示单元测试(unit test),也就是测试的最小单位。例:

javascript 复制代码
describe("样例", function () {
  it("deep用法", function () {
    expect({a: 1}).to.deep.equal({a: 1});
    expect({a: 1}).to.not.equal({a: 1});

    expect([{a: 1}]).to.deep.include({a: 1});
    // expect([{a: 1}]).to.not.include({a: 1});
    expect([{a: 1}]).to.be.include({a: 1});
  });
});

mocha一共四个生命钩子

  • before():在该区块的所有测试用例之前执行
  • after():在该区块的所有测试用例之后执行
  • beforeEach():在每个单元测试前执行
  • afterEach():在每个单元测试后执行

利用describe.skip可以跳过测试,而不用注释大块代码;异步只需要在函数中增加done回调。例:

javascript 复制代码
describe.skip('异步 beforeEach 示例', function () {
  var foo = false;

  beforeEach(function (done) {
    setTimeout(function () {
      foo = true;
      done();
    }, 50);
  });

  it('全局变量异步修改应该成功', function () {
    expect(foo).to.be.equal(true);
  });

  it('read book async', function (done) {
    book.read((err, result) => {
      expect(err).equal(null);
      expect(result).to.be.a('string');
      done();
    })
  });
});

利用mocha,可以轻松组装一个模块的测试逻辑。

chai

chai是断言库,可以理解为比较函数,也就是断言函数是否和预期一致,如果一致则表示测试通过,如果不一致表示测试失败。

本身mocha是不包含断言库的,所以必须引入第三方断言库,目前比较受欢迎的断言库有 should.jsexpect.jschai,具体的语法规则需要大家去查阅相关文档。

因为chai既包含shouldexpectassert三种风格,可扩展性比较强。本质是一样的,按个人习惯选择。详见其api

下面简单的介绍一下3种风格。

should例:

javascript 复制代码
let num = 4+5
num.should.equal(9);
num.should.not.equal(10);

//boolean
'ok'.should.to.be.ok;
false.should.to.not.be.ok;

//type
'test'.should.to.be.a('string');
({ foo: 'bar' }).should.to.be.an('object');

expect例:

javascript 复制代码
// equal or no equal
let num = 4+5
expect(num).equal(9);
expect(num).not.equal(10);

//boolean
expect('ok').to.be.ok;
expect(false).to.not.be.ok;

//type
expect('test').to.be.a('string');
expect({ foo: 'bar' }).to.be.an('object');

assert例:

javascript 复制代码
// equal or no equal
let num = 4+5
assert.equal(num,9);

//type
assert.typeOf('test', 'string', 'test is a string');

就个人喜好而言,比较喜欢函数风格的assert,前两者更像是自然语言。

Jest

Jest 是一个 JavaScript 测试运行器。它允许你使用 jsdom 操作 DOM 。尽管 jsdom 只是对浏览器工作表现的一个近似模拟,对测试前端组件来说它通常也已经够用了。Jest 有着十分优秀的迭代速度,同时还提供了若干强大的功能,上述mocha的功能它基本都有,更提供了丰富的mock能力,比如它可以模拟模块,让你更精细地控制代码如何运行。例:

假定有个从 API 获取用户的类。 该类用 axios 调用 API 然后返回 data,其中包含所有用户的属性:

jsx 复制代码
import axios from 'axios';

class Users {
  static all() {
    return axios.get('/users.json').then(resp => resp.data);
  }
}

export default Users;

现在,为测试该方法而不实际调用 API (使测试缓慢与脆弱),我们可以用 jest.mock(...) 函数自动模拟 axios 模块。

一旦模拟模块,我们可为 .get 提供一个mockResolvedValue,它会返回假数据用于测试。 实际上,我们想说的是我们想让axios.get('/users.json') 有个伪造的响应结果。

jsx 复制代码
import axios from 'axios';
import Users from './users';

jest.mock('axios');

test('should fetch users', () => {
  const users = [{name: 'Bob'}];
  const resp = {data: users};
  axios.get.mockResolvedValue(resp);

  // or you could use the following depending on your use case:
  // axios.get.mockImplementation(() => Promise.resolve(resp))

  return Users.all().then(data => expect(data).toEqual(users));
});

也可以模拟部分模块,mock导出的具体函数,具体请看文档,这里就不赘述了。

正因为Jest的优秀与强大,React、Vue官方都有推荐使用它进行组件单元测试。

端到端测试(E2E)

E2E(end to end)测试是指端到端测试,又叫功能测试,也可以称为集成测试,站在用户视角,使用各种功能、各种交互,是用户的真实使用场景的仿真。

在产品高速迭代的现在,有个自动化测试,是重构、迭代的重要保障。对Web前端来说,主要的测试就是,表单、动画、页面跳转、DOM渲染、Ajax等是否按照期望。

E2E测试正是保障功能的最高层测试,不关注代码实现细节,专注于代码能否实现对应的功能。它会针对你的应用在生产环境下进行网络请求,通常需要建立一个数据库或其它形式的后端,甚至可能针对一个预备上线的环境运行。

对我们开发人员而言,测试的主要关注点是映射到页面的逻辑(比如存储的变量)是否正确。

有个比较常见的问题是,E2E测试是否要连接一个真实的后台API?个人认为,这个还是从业务上根据你的测试目的来决策。如果是要验证整体业务功能,那最好连接一个真实的后台API。如果只是保障前端功能的质量,并且明确业务功能的细节,那么Mock就是个更好的选择(不推荐前端代码中写死各种Mock数据,一般成熟的团队都有接口平台,可充分利用它的Mock功能)。

Nightwatch

我们曾使用Nigthwatch来做E2E测试。

Nightwatch是一个针对Web应用程序和网站的自动化测试框架,使用Node.js并使用W3C WebDriver API(以前称为 "Selenium WebDriver")。它是一个完整的端到端测试解决方案,旨在简化编写自动化测试和设置持续集成的过程。

它的优点是一套代码,可以同时支持FirefoxChromeEdge等浏览器的模拟测试。

Nightwatch的使用很简单,一个nightwatch.json或者nightwatch.config.js(后者优先级高)配置文件,使用runner会自动找同级的这两个文件来获取配置信息。也可以手动使用--config来制定配置文件的相对路径。

具体代码就不贴了,有兴趣的可以参考官方文档进行配置。

Puppeteer

Puppeteer,翻译过来是操纵木偶的人,是谷歌官方出品的一个通过DevTools协议控制headless ChromeNode.js库。

什么是headless?就是无头、无UI界面的隐形浏览器。它包含了浏览器所有功能,操作DOM时不需要打开浏览器,所以天生就适合做自动化测试。

早期前端E2E测试,基本是用PhantomJS,不过其核心开发者在Chrome推出headless模式后就放弃了。因为Puppeteer背后有Chrome团队维护支持,当然有更好的前景。

看下Puppeteer能做些什么?

  1. 利用网页生成PDF、图片。
  2. 抓取 SPA(单页应用)并生成预渲染内容,用作服务端渲染。
  3. 自动化表单提交、UI测试、键盘输入等。
  4. 捕获站点的时间线,以便追踪你的网站,帮助分析网站性能问题。
  5. 测试浏览器扩展。

其实Puppeteer可以是一个优秀的爬虫工具,因为我们熟知的人机防范策略多数是防备非浏览器的,而使用Puppeteer,确确实实就是用浏览器访问的。你可以试着用它玩玩爬虫教程中常见的下载图片。

来看一个用例:

jsx 复制代码
const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto('https://www.baidu.com');
  await page.screenshot({path: 'example.png'});
  await browser.close();
})();

我们安装时一般会下载最新版本的Chromium,该版本在Windows上大约是130Mb。在国内下载速度可想而知,最麻烦有时候可能还下载不下来。所以除了切换镜像,使用cnpm(在写这篇文章时突然没成功)外,可以选择另一种方式------使用官方的puppeteer-core,它会跳过该下载。

  1. 淘宝镜像上找到你的操作系统对应的包下载,放到本地目录,比如当前工程的bin目录。
  2. 这里看 Chromium 和puppeteer 对应的版本,然后npm安装puppeteer-core相应的版本(必须一致)。
  3. 修改刚才的代码,就是把puppeteer换成puppeteer-core,再把puppeteer.launch加一个executablePath参数。
jsx 复制代码
const puppeteer = require('puppeteer-core');
(async () => {
  const browser = await puppeteer.launch({
        executablePath: './bin/chrome.exe'
   });
  const page = await browser.newPage();
  await page.goto('https://www.baidu.com');
  await page.screenshot({path: 'example.png'});
  await browser.close();
})();

Puppeteer可以修改user-agent,给每个Http请求注入headers,所以可以做到普通网页做不到的事情。如:

jsx 复制代码
const page = await browser.newPage();
await page.setUserAgent("xx");
await page.setExtraHTTPHeaders({
  "Authorization": "xxx",
});

更详细的Puppeteer用法与实践,可参见《Deno使用Puppeteer开发实践》。

集成到GitLab的CICD

测试任务当然要集成在开发平台上,以GitLab以例,需要配置一个.gitlab-ci.yml文件。它是运行docker中,也就是一个Linux环境里,这里找到一个alpine镜像------zenika/alpine-chrome:with-puppeteer,大概500M。

有人说直接用npm安装puppeteer不就行了?事实上puppeteer要工作的话,除了需要下载Chromium外,当前环境(比如centos)至少要安装以下软件:

bash 复制代码
alsa-lib.x86_64
atk.x86_64
cups-libs.x86_64
gtk3.x86_64
ipa-gothic-fonts
libXcomposite.x86_64
libXcursor.x86_64
libXdamage.x86_64
libXext.x86_64
libXi.x86_64
libXrandr.x86_64
libXScrnSaver.x86_64
libXtst.x86_64
pango.x86_64
xorg-x11-fonts-100dpi
xorg-x11-fonts-75dpi
xorg-x11-fonts-cyrillic
xorg-x11-fonts-misc
xorg-x11-fonts-Type1
xorg-x11-utils

要支持中文等语言,还有相应的字体文件等得安装,这些放在yml文件里每次都安装一下是很烦琐没有必要的,所以使用镜像是经济划算的选择。

这是一个简单的用例:

yaml 复制代码
stages:
  - test

e2e:
  stage: test
  image: zenika/alpine-chrome:with-puppeteer
  before_script:
    - npm install puppeteer@14.1.0
  script:
    - node main.js
  artifacts:
    paths:
      - example.png

需要注意的是,这时main.js中launch得加上参数--no-sandbox,而且headless一定不能是false。

javascript 复制代码
const browser = await puppeteer.launch({
  args: ['--no-sandbox', '--disable-setuid-sandbox']
 });

总结

本文只是简单介绍了下测试常见的两种分类------单元测试和端到端测试。通常来说,这些环节都可以集成在CICD流程之中,随着每次代码提交而触发测试,保障程序的健壮性。值得一提的是,为方便E2E测试或爬虫,DOM的id、name或class尽可能语义化,也应该保持稳定。

相关推荐
恋猫de小郭4 小时前
Flutter Zero 是什么?它的出现有什么意义?为什么你需要了解下?
android·前端·flutter
崔庆才丨静觅10 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅12 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊12 小时前
jwt介绍
前端