持续集成之测试篇

前言

随着工程化的发展,人力测试已经很难满足生产需要,尤其是某些团队、开源工作者可能并没有专业的测试人员。以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 [email protected]
  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尽可能语义化,也应该保持稳定。

相关推荐
南囝coding30 分钟前
这个 361K Star 的项目,一定要收藏!
前端·后端·github
我不吃饼干30 分钟前
我给掘金写了一个给用户加标签的功能
前端·javascript·cursor
羚羊角uou1 小时前
【C++】模拟实现map和set
java·前端·c++
90后的晨仔1 小时前
ArkTS 与 Swift 闭包的对比分析
前端·harmonyos
小小小小宇1 小时前
前端用户行为监控
前端
步行cgn2 小时前
Vue 事件修饰符详解
前端·javascript·vue.js
vvilkim2 小时前
Flutter 状态管理基础:深入理解 setState 和 InheritedWidget
前端·javascript·flutter
Magnum Lehar2 小时前
wpf3d游戏引擎前端ControlTemplate实现
前端·游戏引擎·wpf
早该学学了2 小时前
el-tabs问题解决大总结
前端
星河丶2 小时前
useEffect的清理函数的执行时机
前端·react.js