前端自动化测试浅析

前言:测试简介

前端常见的问题:

  • 修改某个模块功能时,其它模块也受影响,很难快速定位bug

  • 多人开发代码越来越难以维护

  • 不方便迭代,代码无法重构

  • 代码质量差

增加自动化测试后:

  • 我们为核心功能编写测试后可以保障项目的可靠性

  • 强迫开发者编写更容易被测试的代码,提高代码质量

  • 编写的测试有文档的作用,方便维护

 

测试方法可以分为三个大类黑盒测试、白盒测试、灰盒测试

  • 黑盒测试一般也被称为功能测试,更注重结果的展示,要求测试人员将程序看作一个整体,不考虑其内部结构和特性,只是按照期望验证程序是否能正常工作(就是不知道源代码是什么意思,只是针对界面、bug的测试),现在传统的测试部门主要采用这种测试方式,比较片面化,只能测到看得的东西,一些内部复杂的逻辑可能测不到。

  • 白盒测试是基于代码本身的测试,一般指对代码逻辑结构的测试,更注重数据的流动,注重过程。

  • 灰盒测试是一种集合了白盒测试和黑盒测试的长处的测试方法。

 

相关概念

TDD

TDD是Test Driven Development 的缩写,也就是测试驱动开发。

通常传统软件工程将测试描述为软件生命周期的一个环节,并且是在编码之后。但敏捷开发大师Kent Beck在2003年出版了 Test Driven Development By Example 一书,从而确立了测试驱动开发这个领域。

TDD需要遵循如下规则:

  • 写一个单元测试去描述程序的一个方面。

  • 运行它应该会失败,因为程序还缺少这个特性。

  • 为这个程序添加一些尽可能简单的代码保证测试通过。

  • 重构这部分代码,直到代码没有重复、代码责任清晰并且结构简单。

  • 持续重复这样做,积累代码。

TDD具有很强的目的性,在直接结果的指导下开发生产代码,然后不断围绕这个目标去改进代码,其优势是高效和去冗余的。所以其特点应该是由需求得出测试,由测试代码得出生产代码。打个比方就像是自行车的两个轮子,虽然都是在向同一个方向转动,但是后轮是施力的,带动车子向前,而前轮是受力的,被向前的车子带动而转。

BDD

所谓的BDD行为驱动开发,即Behaviour Driven Development,是一种新的敏捷开发方法。其实可以认为BDD是TDD的一个子集或分支,是测试驱动开发的升级版,它更趋向于需求,需要共同利益者的参与,强调用户故事(User Story)和行为。2009年,在伦敦发表的“敏捷规格,BDD和极限测试交流”中,Dan North对BDD给出了如下定义:

BDD是第二代的、由外及内的、基于拉(pull)的、多方利益相关者的(stakeholder)、多种可扩展的、高自动化的敏捷方法。它描述了一个交互循环,可以具有带有良好定义的输出(即工作中交付的结果):已测试过的软件。

它对TDD的理念进行了扩展,在TDD中侧重点偏向开发,通过测试用例来规范约束开发者编写出质量更高、bug更少的代码。而BDD更加侧重设计,其要求在设计测试用例的时候对系统进行定义,倡导使用通用的语言将系统的行为描述出来,将系统设计和测试用例结合起来,从而以此为驱动进行开发工作。

大致过程:

  1. 从业务的角度定义具体的,以及可衡量的目标

  2. 找到一种可以达到设定目标的、对业务最重要的那些功能的方法

  3. 然后像故事一样描述出一个个具体可执行的行为。其描述方法基于一些通用词汇,这些词汇具有准确无误的表达能力和一致的含义。例如,expect, should, assert

  4. 寻找合适语言及方法,对行为进行实现

  5. 测试人员检验产品运行结果是否符合预期行为。最大程度的交付出符合用户期望的产品,避免表达不一致带来的问题

 

测试的分类

  • 单元测试(Unit Testing)

  • 集成测试(Integration Testing)

  • 端到端测试(E2E Testing)

一、单元测试(Unit Test)

参考链接:

https://www.jianshu.com/p/bb713d2fe3ad

1、什么是单元测试

  • 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试,属于白盒测试。

 

对于JavaScript来说,通常也是针对函数、对象和模块的测试

2、JavaScript单元测试现状

单元测试在后台开发中非常流行和普及,比如JAVA开发者的JUnit等,而在前端开发中则使用的非常少。究其原因,主要是单元测试更适用于逻辑代码的测试,这对于JAVA等后台编程语言来说测试起来非常方便,但是前端开发很多时候要要UI打交道,UI相关的代码不是不可以进行单元测试,但的确很麻烦,比起逻辑代码来说困难多了,这就导致了单元测试在前端开发没有普及起来。

但是随着单元测试的普及,尤其是敏捷开发的推动,涌现了许多优秀的JavaScript单元测试框架,如QUnit、Jasmine等。所有的这些框架基本上都能对Javascript代码进行很好的测试,当然UI部分的代码测试一样比较麻烦,但是我们可以通过精心构造我们的测试代码来测试部分UI代码。

3、为什么要进行单元测试

经验表明一个尽责的单元测试方法将会在软件开发的某个阶段发现很多的Bug,并且修改它们的成本也很低。在软件开发的后期阶段,Bug的发现并修改将会变得更加困难,并要消耗大量的时间和开发费用。无论什么时候作出修改都要进行完整的回归测试,在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。在提供了经过测试的单元的情况下,系统集成过程将会大大地简化。开发人员可以将精力集中在单元之间的交互作用和全局的功能实现上,而不是陷入充满很多Bug的单元之中不能自拔。

 

4. 如何进行单元测试

4.1 选择测试工具

在JavaScript世界中,我们需要至少三个工具来进行单元测试,这意味着每个工具都需要你进行选择:

  • 测试管理工具测试管理工具是用来组织和运行整个测试的工具,它能够将测试框架、断言库、测试浏览器、测试代码和被测试代码组织起来,并运行被测试代码进行测试。测试工具有很多选择,Selenium、WebDriver/Selenium 2、Mocha[1]、JsTestDriver、HTML Runners和Karma,我这里选择使用Karma

karma:Google Angular 团队写的,功能很强大,有很多插件。可以连接真实的浏览器跑测试。能够用一些测试覆盖率统计的工具统计一下覆盖率;或是能够加入持续集成,提交代码后自动跑测试用例。

 

  • 测试框架测是框架是单元测试的核心,它提供了单元测试所需的各种API,你可以使用它们来对你的代码进行单元测试。JavaScript的测试框架可谓百花齐放,选择太多了(可以参考List of unit testing frameworks),我这里选择使用Mocha(关于它们中一些框架的对比,可以参考javascript单元测试)

Jasmine:自带断言(assert),mock功能

Mocha:框架不带断言和mock功能,需要结合其他工具,由tj大神开发

Jest:由Facebook出品的测试框架,在Jasmine测试框架上演变开发而来

 

  • 断言库断言库提供了用于描述你的具体测试的API,有了它们你的测试代码便能简单直接,也更为语义化,理想状态下你甚至可以让非开发人员来撰写单元测试。所谓的断言,就是预期某些执行结果符合你自己的要求。所有的测试用例都应该含有一句或多句的断言。当然,你也完全可以不使用断言库,而是用自己的测试代码去测试,不过几乎没有人会这么干,除非你自己实现了一个测试断言库。测试断言库的选择也不少:better-assert、should.js、expect.js、chai.js等等(有关它们的对比,可以参考几款前端测试断言库(Assertions lib)的选型总结)我这里选择chai.js

chai: 目前比较流行的断言库,支持 TDD(assert),BDD(expect、should)两种风格

should.js:也是tj大神所写

  • mock库

参考链接:

https://www.jianshu.com/p/bb713d2fe3ad

sinon.js:使用Sinon,我们可以把任何JavaScript函数替换成一个测试替身。通过配置,测试替身可以完成各种各样的任务来让测试复杂代码变得简单。支持 spies, stub, fake XMLHttpRequest, Fake server, Fake time,很强大

 

有了上面的四个工具,你就可以开始对你的node代码进行测试了。但是如果想要对前端代码进行测试,还需要另外一个工具:

  • 测试浏览器前端代码是运行在浏览器中的,要对其进行单元测试,只能将其运行在浏览器上。目前大部分测试工具都支持调用和运行本地浏览器来进行测试,但如果你的测试仅仅是针对函数和模块的单元测试,则完全可以使用一款无界面的浏览器:PhantomJs

另外,还有一个很重要的事情就是测试覆盖率的统计。一般情况下你的测试管理工具会提供相关的覆盖率统计工具,但是有些情况下它们提供的工具未必是你想要的。比如当被测试的代码是经过了某些打包工具打包完了且被压缩和混淆了,同时打包工具还混入了很多自己的代码,这时覆盖率的统计就容易不准确。所以为了避免这种情况,测试覆盖率统计工具需要谨慎选择,至少你得确认它支持你的打包工具已经打包好的代码。

 

  • 测试覆盖率统计工具

Karma-Coverage是Karma官方提供的覆盖率统计插件,自然成为项目的首选。

 

参考网站:

https://www.cnblogs.com/baoshuyan66/p/9937087.html

 

  • karma-webpack 用webpack预处理文件

  • karma-coverage 测试覆盖率

  • karma-mocha 接入mocha测试框架

  • karma-spec-reporter 输出报告

  • karma-phantomjs-launcher 控制PhantomJS

  • karma-phantomjs-shim 给PhantomJS兼容的控制

 

4.2 构建一个最基本的测试

参考网站:

https://www.jianshu.com/p/6726c0410650#fn1

 

describe('index.js的测试', function () {
  it('1应该是数字', function() {
      // expect(isNum(1)).to.be.true
      isNum(1).should.equal(true)
  })
  it('"1" 应该是字符', function() {
      // expect(isString('1')).to.be.true
      isString('1').should.equal(true)
  })


})

describe块称为"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称("index.js的测试"),第二个参数是一个实际执行的函数。

it块称为"测试用例"(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称("1应该是数字"),第二个参数是一个实际执行的函数。

 

其中,describe和it是mocha的语法结构,describe是这对某个组件或者函数的名字描述,测试脚本里面应该包括一个或多个describe块,每个describe块应该包括一个或多个it块。it是对它需要完成某些功能的描述,它里面是具体的测试用例。在测试框架中,describe,it, expect和sinon都是全局方法。

expect(isString('1')).to.be.true是Chai下的语法

 

二、集成测试(Integration Testing)

1、什么是集成测试

 

  • 将已测试过的单元测试函数进行组合集成暴露出的高层函数或类的封装,对这些函数或类进行的测试,属于白盒测试

选用Travis CI(免费)或Circle CI来完成持续集成测试

本地根目录创建名为 .travis.yml 的Travis配置文件

 

阮一峰教程:

http://www.ruanyifeng.com/blog/2017/12/travis_ci_tutorial.html

 

三、端到端测试(E2E Testing)

参考链接:

https://blog.csdn.net/sinat_33312523/article/details/82955514

1、什么是端到端测试

  • End to End Testing, 打开应用程序模拟输入,检查功能以及界面是否正确,像是一个自动化的测试脚本,模拟用户行为。在Web应用程序中,他们会启动服务器,打开浏览器,去自动点击一个真实浏览器环境中的页面,再通过直接抓取页面上的DOM来断言是否符合预期,这是最接近用户的测试方式,从一定程度而言端到端测试对于一个产品的发布起到了至关重要的作用,这直接关系到了DOM给用户带来的视觉上的交互。不同于行为驱动测试(BDD)和单元测试独立运行并使用模拟/存根,端到端测试将试着尽可能从用户的视角,对真实系统的访问行为进行仿真。对Web应用来说,这意味着需要打开浏览器、加载页面、运行JavaScript,以及进行与DOM交互等操作。属于灰盒测试

2、框架介绍

官网:https://www.cypress.io/

Cypress是基于 electron 的一个测试框架,提供 web 环境进行点对点的测试,在 programer 思维下,进行自动化的交互操作,必要点检测说明,这是一个非常棒的用处。例如之后拥有数据埋点,可以在固定的位置检测是否有埋点。

 

2.1 Cypress四个特性

 

  • 时间旅行

Cypress在您的测试运行时拍摄快照。只需将鼠标悬停在命令日志中的命令上即可确切了解每一步中发生的情况。不仅仅是每个测试用例都会出现在测试页面的左侧,每一次浏览器的行为都会被记录在一个类似Timeline的列表中,在鼠标移入后右侧页面将会停留在一个那一个瞬间形成一个快照库,我认为这是一个非常棒的功能,这更适合我们来定位bug所发生的时间点和位置。

 

  • 可调试

不用再去猜测为什么你的调试失败了。因为这款框架是基于electron来编写的,所以用来承载它运行的为Chrome浏览器,也就是说我们的应用是跑在一个真实的浏览器的环境中,所以在我们的页面出现问题的时候,我们同样可以调用熟悉的F12控制台去debug

 

  • 实时重新加载

不论你何时更改了测试内容,Cypress都会自动热加载,这样你就能随时在应用中看到所执行的命令

 

  • 自动等待

Cypress会自动等待命令和断言。这也是一个非常讨喜的功能,在使用Jest中,我时常需要调用Vue自身所携带的异步等待函数vue.nextTick来进行下一步的断言。在Cypress中我们完全不需要担心异步等待在代码中的呈现,框架会自动等待回调的结束,在这个过程中并且会启动一个计时。

 

2.2 Cypress配置安装

  • 目录结构

|-- tests                            // 测试目录
|   |-- __mocks__                    // 单元测试模拟数据
|   |-- coverage                   
|   |-- e2e                          // E2E测试
|   |   |-- config                   // 请求路径配置
|   |   |   |-- conf.js
|   |   |-- fixtures                 // 模拟请求文件
|   |   |-- plugins                  // 主要配置
|   |   |   | -- index.js                
|   |   |-- specs                    // 测试用例
|   |   |   | -- xxx.spec.js        
|   |   |-- support                  // 配置自定义命令
|   |   |   | -- commands.js
|   |   |   | -- index.js
|   |-- unit                         // 单元测试
|   |   |-- xxx.spec.ts              // 测试用例

 

在根目录下还需要一个cypress.json

{
  "pluginsFile": "tests/e2e/plugins/index.js",
  "projectId": "8efjtr"
}

 

我们这里一个个来介绍文件的目录结构和配置说明

 

cypress.json

首先在启动整个Cypress时,会在项目的根目录中去寻找这个文件,在vue-cli-3中单独把pluginsFile这个配置项指向了我们重新配置的路径(实际在安装完一个Cypress项目时,测试用例所在的目录就是cypress文件夹),在这个被指向的配置文件中再去使用config参数配置其他目录所在的位置:

// https://docs.cypress.io/guides/guides/plugins-guide.html
module.exports = (on, config) => {
 return Object.assign({}, config, {
   // baseUrl: "http://localhost:8080", // 测试域名
   fixturesFolder: 'tests/e2e/fixtures', 
   integrationFolder: 'tests/e2e/specs', // 测试文件文件夹
   screenshotsFolder: 'tests/e2e/screenshots', // 屏幕快照
   // videoRecording: true,
   videosFolder: 'tests/e2e/videos', // 录制后的文件夹
   supportFile: 'tests/e2e/support/index.js'
 })
}

当然我们也可以直接在cypress.json中去指定这些配置:

{
 "projectId": "by9ntm",
 "fixturesFolder": "test/e2e/fixtures",
 "integrationFolder": "test/e2e/specs",
 "pluginsFile": "test/e2e/plugins",
 "screenshotsFolder": "test/e2e/screenshots",
 "supportFile": "test/e2e/support",
 "videosFolder": "test/e2e/videos",
 "baseUrl": "http://localhost:8080"
}

生成配置后可以在项目启动后的dashboard的setting选项中看到所有我们对配置的改动,并会通过不同颜色进行标注。同样这个json的文件还可以对不同环境下的配置做不同的更改,具体参照官方文档。

 

  • config

在这个文件夹中我主要做了一个当前环境的判断配置,由于Cypress是浏览器真实去访问一个链接的地址,所以需要把整个项目给启动后才可以去测试,所以需要一个准确的baseUrl

  • fixtures

这个文件夹很有意思,是为了存放模拟上传或读取的文件,在场景中我们经常会碰到文件的拖拽上传或者下载等等,这些操作在单元测试是非常难以测试的,所以我们需要在端到端测试中把这些操作模拟掉,这个文件夹就是用来存放这类特殊文件的。

  • specs

顾名思义所有的测试用例都要放在这个下面。

  • support

还是回到上传文件这个场景,很多特殊场景的情况下Cypress的API很可能没有覆盖到,所以可以在这个文件夹下配置自定义的命令全局注入到框架中使用。

 

基础配置

/plugins/index.js

// https://docs.cypress.io/guides/guides/plugins-guide.html
module.exports = (on, config) => {
  return Object.assign({}, config, {
    // baseUrl: "http://localhost:8080", // 测试域名
    fixturesFolder: 'tests/e2e/fixtures', 
    integrationFolder: 'tests/e2e/specs', // 测试文件文件夹
    screenshotsFolder: 'tests/e2e/screenshots', // 屏幕快照
    // videoRecording: true,
    videosFolder: 'tests/e2e/videos', // 录制后的文件夹
    supportFile: 'tests/e2e/support/index.js',
    viewportHeight: 768, // 测试浏览器视口高度     
    viewportWidth: 1366 // 测试浏览器视口宽度
  })
}

运行测试

在vue-cli 3.0的默认配置中,我们直接运行npm run test:e2e来启动我们的测试,在这之前我们需要先启动我们的项目和mock服务,最后去启动我们的E2E,否则baseUrl会请求不到正确的端口。

这是启动页面,顶部会有三个选项配置可供选择,在</>Tests选项中我们可以看到我们所有的spec,当然这个顺序是按照字母排列的,我们可以点击Run all specs按钮来一次性启动所有的用例,也可以点击单个用例来启动。

 

 

在Runs选项中我们可以看到和CI集成所生成的项目ID

 

在Setting选项中我们可以看到merge之后所有的配置

 

运行页面

这是一次成功的测试,可以看到左边的列表都已经呈现一个绿色的状态,鼠标选中可以看到当时页面的一个状态。

 

2.3 语法实战

在我们写端到端测试之前,我们应该明确我们是基于一个用户的角度去测试我们的页面,所以这无关我们的所有源码,我们应该只专注于浏览器所呈现给我们的资源,包括页面上的element、控制台中network中的所有的请求以及导航栏上的url信息,这是我们可以去测试和观察到的所有的点。

  • spec基本结构

// https://docs.cypress.io/api/introduction/api.html
import { DEV_SERVER } from '../config/conf'
describe('主页', () => {
  it('Home', () => {
    cy.visit('/')
    cy.contains('h1', 'QDeploy 智能安装部署平台')
    cy.get('button').click()
    cy.url().should('eq', `${DEV_SERVER}steps/selectMode`)
  })
})

 

这里举一个最简单的例子,和单元测试一样,首先要把所有的用例包裹在一个describe中

 

1、在用例中先用cy.visit()方法访问地址,这里后面只加了/是因为baseUrl已经设置过了的原因。

2、使用cy.contains()或者cy.get()去抓取DOM并进行断言,Cypress中默认包含的断言库为Chai。

3、由于设有异步等待的机制,所以我们可以毫无顾及地去写下一步的操作,包括button的点击事件和跳转之后url的判断。

 

  • 生命周期

在一个测试集合中,我们也可以加入自身的生命周期,这些生命周期主要是针对每个测试用例来执行的,包括beforeEach、beforeAll、afterEach、afterAll

我在这个测试集合中主要用到了beforeEach这个声明周期,在每个测试用例开始之前我都对我需要的DOM进行抓取并取一个别名,这样我方便其他用例需要时就不需要再反复去寻找这个节点对象了。

 beforeEach(() => {
   cy.visit('/#/steps/selectMode')
             cy.get('.one__item__right').eq(0).find('.item__right__btn').eq(0).as('hasConfigFile')
       cy.get('.one__item__right').eq(0).find('.item__right__btn').eq(1).as('notConfigFile')
       cy.get('.one__item__right').eq(1).find('.item__right__btn').eq(0).as('hasSystem')
       cy.get('.one__item__right').eq(1).find('.item__right__btn').eq(1).as('notSystem')
   cy.get('.btn__next').as('next')
   cy.get('.item__upload__text').as('fileName')
 })

在取了别名之后其他用例只需要调用cy.get('@name')就可以取到相应别名的DOM元素。

  • 模拟请求

在我们测试的时候总是会免不了一些请求的发出,在Cypress中由于是真实的浏览器环境,所以所有的请求都会被正常发出,但是有些时候我们需要mock掉一些请求来观察DOM的反馈是否符合预期,这里就需要引入一个比较重要的概念——存根stub。

不同于单元测试的mock,我认为在单元测试中更类似于axios中的拦截器,对整个请求的代码层面进行一个拦截后返回一个相同格式的对象骗过,而在端到端测试中因为我们无法对项目本身的源码下手,所以我们只能从浏览器层面去模拟,在这里的存根我的理解是在页面发出请求之前,先对一个API做一个标记,当浏览器触发这个方法并发送请求后使用标记后的模拟请求返回并进行后续的断言操作,我们来看一下代码。

describe('installSystem', () => {
  it('寻找节点失败', () => {
    cy.server()
    cy.route({
      method: 'DELETE',
      url: 'api/find/node',
      status: 200,
      response: {
        data: {},
        error_code: 1,
        message: 'error'
      }
    })
    cy.visit('/#/steps/installSystem')
    cy.wait(1000)
    cy.get('.pop_content_confirm').find('div').find('div').contains('寻找节点出错')
  })
})

在这个例子中,由于请求在页面刚被挂载后就被触发了,也就是说整个请求是写在mounted这个声明周期中的,所以我们需要在访问页面之前就对这个需要被mock的api做一个stub。

 

1、首先我们使用cy.server()声明一个mock的请求。

2、然后使用cy.route()去描述我们需要模拟的api的具体信息,在里面可以填写很多的配置,包括请求的方法method,请求的地址url,请求返回的状态码status以及最后返回的response body,在这里由于项目本身中还定义了error_code状态码,所以对于这一个请求所具备的状态我们就需要写很多个测试用例的组合去断言是否符合我们的预期。

3、然后因为请求已经被我们存根了,我们再去使用cy.visit()访问一次页面就可以看到我们所需要被模拟的请求已经被存根并且成功模拟了。

 

 

到目前为止我们就已经非常成功地对一个API进行模拟请求了,对比起单元测试还是方便了不少的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值