# 进阶 11 前端自动化测试

**测试**是在开发完成的应用程序之上采用**人工或非人工**的方式验证应用是否有错误和缺陷、是否符合工程预期、是否会造成用户/开发商损失等潜在问题的一种方式。

**自动化测试**是采用一种**非人工**的测试方式，即使用软件工具或运行一段特定的测试代码，去验证目标代码是否满足期望。**前端自动化测试**一般是在预设条件下运行前端页面或逻辑模块，预设条件应包括正常条件和异常条件，以达到自动运行测试过程、减少或避免人工干预测试的目的，然后评估运行结果。

测试的目的是为了提升代码的质量、可靠性、可维护性，自动化测试则是将人工测试行为转化为由机器自动执行测试的行为，从而替代大量的手工测试操作，使测试**能更加快速高效且准确定位问题、并且能反复进行**。

## 为什么需要自动化测试？

大多数情况下，前端代码都是开发者人工自测，又或是提测后由专门的测试人员人工测试。

人工测试能快速准确的获得视觉反馈，并且人的判断力和直觉总是有益于发现应用的一些缺陷或体验问题，但人工容易出现人为的失误或遗漏，另外还有最重要的一点，**不利于后期的回归测试**。自动化测试可找到比人工测试更多的错误，可以快速高效地进行测试，并且可以重用和执行相同类型的测试操作，它的缺点是会增加测试成本，如测试软件的费用，或者开发维护测试代码的成本。

自动化测试的优势：

* 可以验证代码正确性，保证项目质量；
* 测试用例可以复用，一次编写，多次运行；
* 通过看测试用例可以快速了解需求；
* 驱动开发，指导设计，保证写的代码可测试。

```shell
自动化的收益 = 迭代次数 * 全手动执行成本 - 首次自动化成本 - 维护次数 * 维护成本
```

## 什么项目适合自动化测试？

自动化测试有很多优点，但并不是所有项目都适合自动化测试，主要原因是**自动化测试的成本问题**。在实施自动化测试以前需要对软件开发过程进行分析，基于投入产出来判断是否适合实施自动化测试。

一般需要同时满足以下条件：

* 任务测试明确，需求变更不频繁；
* 项目周期足够长；
* 自动化测试脚本可重复使用；
* 比较频繁的回归测试；
* 开发比较规范，具有可测试性。

若是需求变更过于频繁，维护测试脚本的成本过高；若是项目周期比较短，没有足够的时间去支持自动化测试的过程；若是测试脚本重复使用率低或者后期不需要频繁的回归测试，耗费的精力大于创造的价值，不值得；若是代码不规范，可测试性差，那自动化测试实施起来会比较困难。

## 测试的方法

测试方法一般分为黑盒测试、白盒测试和灰盒测试。

### 黑盒测试

黑盒测试，也称黑箱测试，是软件测试方法，测试应用程序的功能，而不是其内部结构或运作。测试者不需具备应用程序的代码、内部结构和编程语言的专门知识。测试者只需知道什么是系统应该做的事，即当键入一个特定的输入，可得到一定的输出。测试案例是依应用系统应该做的功能，按照规范、规格或要求等设计。测试者选择有效输入和无效输入来验证是否正确的输出。

此测试方法可适合大部分的软件测试，例如集成测试、系统测试。

### 白盒测试

白盒测试，又称透明盒测试、结构测试，是一个测试软件的方法，测试应用程序的内部结构或运作，而不是测试应用程序的功能（即黑盒测试）。在白盒测试时，以编程语言的角度来设计测试案例。测试者完全知道程序的结构和处理算法，按照程序内部逻辑设计测试用例，检测程序中的主要执行通路是否能按照预定要求正确工作，类似测试电路中的节点。

此测试方法可适合单元测试、集成测试和系统的软件测试流程，可测试在集成过程中每一单元之间的路径，或者主系统跟子系统中的测试。

### 灰盒测试

灰盒测试，是介于白盒测试与黑盒测试之间的，可以这样理解，灰盒测试关注输出对于输入的正确性，同时也关注内部表现，但这种关注不象白盒那样详细、完整，只是通过一些表征性的现象、事件、标志来判断内部的运行状态，有时候输出是正确的，但内部其实已经错误了，这种情况非常多，如果每次都通过白盒测试来操作，效率会很低，因此需要采取这样的一种灰盒的方法。

## 测试工作流程

按照软件工程自底而上的概念，前端测试一般分为单元测试、集成测试、端到端（E2E）测试。

### 单元测试

单元测试，也称模块测试，通常可放在编程阶段，由程序员对自己编写的模块自行测试，检查模块是否实现了详细设计说明书中规定的功能和算法。单元测试主要发现编程和详细设计中产生的错误，单元测试计划应该在详细设计阶段制定。

单元测试是指对程序中**最小可测试单元**进行的，如一个函数、一个模块、一个组件等，着重从以下方面测试：模块接口、局部数据结构、重要的执行通路、出错处理通路及边界条件等。

前端单元测试和后端单元测试最大的区别在于：前端单元测试无法避免的会存在兼容性问题，如浏览器兼容性API、BOM API 的调用，因此前端单元测试需要运行在（伪）浏览器环境下。

单元测试的解决方案：

* Mocha：是一个专注于灵活性的 JavaScript 测试框架。因为其灵活性，它允许你选择不同的库来满足诸如侦听（Sinon）和断言（Chai）等其它常见的功能。另一个 Mocha 独特的功能是它不止可以在 Node.js 里运行测试，还可以在浏览器里运行测试。
* Jest：是一个专注于简易性的 JavaScript 测试框架。一个其独特的功能是可以为测试生成快照 (snapshot)，以提供另一种验证应用单元的方法。
* Ava：更轻量高效简单的单测框架，但自身不够稳定，并发运行文件多的时候会撑爆 CPU。
* Jasmine：单测框架的“元老”，开箱即用，但是异步测试支持较弱。
* Karma：能在真实的浏览器中测试，强大适配器，可配置其他单测框架，一般会配合 Mocha、Jasmine 等一起使用。

每个框架都有自己的优缺点，没有最好的框架，只有最适合的框架。Augular 默认的是 Karma + Jasmine，而 React、Vue 默认的是 Jest。

### 集成测试

集成测试，也称组装测试、综合测试、联合测试，是指在单元测试的基础上，对由已通过单元测试的模块组装而成的程序进行正确性检测的测试，主要目标是发现**模块间的接口和通信问题**，检测模块组合在一起是否能正常工作、代码工作是否符合预期。集成测试最大的难点就是颗粒度较大，逻辑更加复杂，外部因素更多，无法保证测试的可控和独立性。

集成测试常用在：耦合度较高的函数/组件、经过二次封装的函数/组件、多个函数/组件组合而成的函数/组件等。

Vue 组件测试推荐 [Vue Testing Library](https://testing-library.com/docs/vue-testing-library/intro/)、[Vue Test Utils](https://v1.test-utils.vuejs.org/zh/)。

（Unit Test）有 Mocha, Ava, Karma, Jest, Jasmine 等。

### 端到端 (E2E，end-to-end) 测试

端到端测试可以说从应用最重要的方面进行测试覆盖：当用户实际使用应用时会发生什么。

换句话说，端到端测试是站在用户的角度进度的，验证应用中的所有层，包括前端代码、后端服务和相应的基础设施，它们更能代表你的用户所处的环境。通过测试用户操作如何影响应用，端到端测试通常是提高应用是否正常运行的信心的关键。

对于前端，端到端测试的一个主要优点是它能够跨多个浏览器测试应用。

端到端测试解决方案：

* [Cypress.io](https://docs.cypress.io/guides/overview/why-cypress)：一个测试框架，旨在通过使开发者能够可靠地测试他们的应用，同时提供一流的开发者体验，来提高开发者的生产率。
* [Nightwatch.js](https://nightwatchjs.org/api/)：一个端到端测试框架，可用于测试 web 应用和网站，以及 Node.js 单元测试和集成测试。
* [Puppeteer](https://developers.google.com/web/tools/puppeteer/get-started)：一个 Node.js 库，它提供高阶 API 来控制浏览器，并可以与其他测试运行程序 (例如 Jest) 配对来测试应用。
* [TestCafe](https://testcafe.io/documentation/402635/getting-started#installing-testcafe)：一个基于端到端的 Node.js 框架，旨在提供简单的设置，以便开发者能够专注于创建易于编写和可靠的测试。

## [Mocha](https://mochajs.cn/)

mocha 是一个功能丰富的 javascript 测试框架，运行在 node.js 和浏览器中，使异步测试变得简单有趣。Mocha测试连续运行，允许灵活和准确的报告，同时将未捕获的异常映射到正确的测试用例。

安装：`npm i mocha -S`。

创建一个测试 Js 文件：

```javascript
// test/index.js
const assert = require('assert')
describe('Array', function() {
    describe('#indexOf()', function() {
        it('should return -1 when the value is not present', function() {
            assert.equal([1,2,3].indexOf(4), -1)
        })
    })
})
```

然后在终端运行：`./node_modules/mocha/bin/mocha`。

mocha 命令的基本格式是：

```shell
mocha [debug] [options] [files]
```

```shell
mocha # 默认运行当目录下 test 目录里面的测试脚本，不包括子文件
mocha add.test.js # 当前目录下面的该测试脚本
mocha file1 file2 file3 # mocha命令后面紧跟测试脚本的路径和文件名，可以指定多个测试用例
mocha spec/{my,awesome}.js
mocha test/unit/*.js
mocha –recursive # 执行test子目录下面的所有的测试用例
mocha –watch # -w 监控执行
mocha –timeout 5000 timeout.test.js # -t 异步测试中需要，默认时间为 2000
```

执行结果：

```shell
  Array
    #indexOf()
      √ should return -1 when the value is not present


  1 passing (3ms)
```

## [Jest](https://www.jestjs.cn/)

Jest 是一个令人愉快的 JavaScript 测试框架，专注于 简洁明快。

Jest 的目标是在大部分 JavaScript 项目上实现开箱即用， 无需配置。

安装：`npm i jest -S`。

创建一个测试 Js 文件：

```javascript
// test/index.test.js
function sum (a, b) {
    return a + b
}

test('adds 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).toBe(3)
})
```

然后终端执行：`./node_modules/jest/bin/jest.js`。

```shell
jest <regexForTestFiles>
# jest 附加参数会以正则表达式来匹配项目中的文件
```

```shell
jest # 默认正则 **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?(x)
jest my-test # 指定目录测试
jest path/to/my-test.js # 指定文件测试，文件名需符合默认的正则匹配。
```

执行结果：

```shell
PASS test/index.test.js
  √ adds 1 + 2 to equal 3 (2 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.628 s, estimated 2 s
Ran all test suites.
```

## [Cypress.io](https://www.cypress.io/)

Cypress.io 是一个测试框架，旨在通过使开发者能够可靠地测试他们的应用，同时提供一流的开发者体验，来提高开发者的生产率。

[Cypress 学习指南](https://rualc.com/frontend/cypress/#cypress-jian-jie)

安装：`npm i cypress -S`。

终端运行：`./node_modules/cypress/bin/cypress open`，初始化并生成一堆用例（目录结构如下），并打开 Cypress 的界面。如果已初始化，则直接打开 Cypress。

```shell
├─cypress
|  ├─fixtures
|  │      example.json
|  │
|  ├─integration
|  │  ├─1-getting-started
|  │  │      todo.spec.js
|  │  │
|  │  └─2-advanced-examples
|  │          actions.spec.js
|  │          aliasing.spec.js
|  │          assertions.spec.js
|  │          connectors.spec.js
|  │          cookies.spec.js
|  │          cypress_api.spec.js
|  │          files.spec.js
|  │          local_storage.spec.js
|  │          location.spec.js
|  │          misc.spec.js
|  │          navigation.spec.js
|  │          network_requests.spec.js
|  │          querying.spec.js
|  │          spies_stubs_clocks.spec.js
|  │          traversal.spec.js
|  │          utilities.spec.js
|  │          viewport.spec.js
|  │          waiting.spec.js
|  │          window.spec.js
|  │
|  ├─plugins
|  │      index.js
|  │
|  └─support
|	commands.js
|	index.js
cypress.json
```

![image-20220325221021000](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20220325221021000.png)

测试文件示例：

```javascript
// cypress\integration\test\index.js
describe('端到端测试：示例', () => {
    before(() => console.log('开始：'));
    beforeEach(() => cy.visit('https://example.cypress.io/todo'));

    it('标题文案：todos', () => {
        cy.get('.todoapp .header h1').should('have.text', 'todos')
    })

    it('有两个li节点', () => {
        cy.get('.todo-list li').should('have.length', 2)
    })

    it('输入文案：lizhao', () => {
        cy.get('.todoapp .header input').type('lizhao')
        cy.get('.todoapp .header input').should('have.value', 'lizhao')
    })

    it('比较', () => {
        expect(2 + 2).to.equal(4)
    })
});
```

测试文件默认位于`cypress/integration`，点击该目录下任意 Js 文件，可以在浏览器中模拟用户操作，并在浏览器中显示测试结果。

![image-20220325222904428](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/image-20220325222904428.png)

Cypress 中内置的断言包含了几种类型：

* Chai：断言。

  ```javascript
  expect('test').to.be.a('string') // BDD 风格
  assert.equal(3, 3, 'vals equal') // TDD 风格
  ```
* Chai jQuery：关于 DOM 的断言。

  ```javascript
  expect(\$el).to.have.attr('foo', 'bar')
  ```
* Sinon-Chai：关于函数调用情况的断言。

  ```javascript
  expect(spy).to.be.called
  ```
* .should()：在 Cypress 中封装了以上所有可用断言。

  ```javascript
  cy.get('li.selected').should('have.length', 3) // should
  cy.get('div').should(($div) => { expect($div)... }) // BDD
  ```

## Nightwatch.js

Nightwatch.js是一个集成的、易于使用的 Web 应用程序和网站的端到端测试解决方案，用 Node.js 编写。它使用W3C WebDriver API来驱动浏览器并对 DOM 元素执行命令和断言。

安装：`npm i -S nightwatch`。

安装浏览器驱动程序：`npm i -S geckodriver chromedriver safaridriver` 。**注意：** Edge 驱动程序可以从Microsoft Edge 驱动程序官方主页下载；在使用 safaridriver 之前，您需要运行一次以下命令：`safaridriver --enable`。

创建测试文件：

```javascript
describe('Ecosia', function() {
    it('demo test', function(browser) {
        browser
            .url('https://www.ecosia.org/')
            .setValue('input[type=search]', 'nightwatch')
            .click('button[type=submit]')
            .assert.containsText('.mainline-results', 'Nightwatch.js');
    });

    it('比较', () => {
        expect(2 + 2).to.equal(4)
    })
});
```

终端执行：`./node_modules/nightwatch/bin/nightwatch --env chrome test/index.js`。

## 参考资料

[Vue 测试](https://cn.vuejs.org/v2/guide/testing.html)

[前端自动化测试概览](https://juejin.cn/post/6844903621931302920)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://lizh.gitbook.io/knowledge/frontend/02-jin-jie-11-qian-duan-zi-dong-hua-ce-shi.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
