# 任务 1：练习 API 契约测试

Hey，你好，我是 JimmyLv 吕靖，这是你开始 前端 TDD 练习的第 15 天。今天你将会开始练习 Bookshelf 项目，我们会从一个简单的 RESTful API 入手，使用契约驱动开发的方式模拟和验证前后端协作。

在团队内部，前后端协作本质上需要的不是一份 API 文档，而是一个可以供前后端共同遵守的契约。前后端开发人员可以一起制定一份契约，使用这份契约共同开发，前端使用这份契约启动 mock API，后端则可以通过它简单的验证 API 是否正确输出想要的数据。

## 什么是契约测试？

![API 契约（API Contract）](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M0Doa0ZBxLtz0KDOt4Y%2F-M0DocQcypxZGOwtUiZi%2Fcontract-mocks.jpg?generation=1581870389291409\&alt=media)

契约测试本质上也是一种 TDD，契约驱动开发，双方先定契约，你做你的 我做我的，契约可以自动化执行，检测双方是否达成契约要求。

Pact 测试又名契约测试，是消费者服务与生产者服务之间的消费者驱动测试，消费者驱动的契约测试（[Consumer-Driven Contracts](https://martinfowler.com/articles/consumerDrivenContracts.html)）。

契约可以理解为反向的录像机（VCR）。 VCR 记录实际的提供者行为，并验证使用者的行为是否符合预期。 Pact 记录消费者行为，并验证提供者的行为是否符合预期。

```bash
git clone https://github.com/JimmyLv/tdd-bookshelf.git
cd tdd-bookshelf

yarn install #安装依赖
yarn start #启动应用
```

当然，我还通过 CodeSandbox 的方式作为你的快速入口：<https://codesandbox.io/s/github/JimmyLv/tdd-bookshelf> (可能需要科学上网)，帮助你去除设置开发环境的障碍，一键打开 Cloud IDE 即可开始编码，右边则是你的网站运行效果预览，比如：<https://tdd-bookshelf.jimmylv.now.sh/>

{% embed url="<https://codesandbox.io/embed/github/JimmyLv/tdd-bookshelf/tree/master/?autoresize=1&fontsize=14&hidenavigation=1&theme=dark>" %}
CodeSandbox 代码编辑
{% endembed %}

[![Edit tdd-bookshelf](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/JimmyLv/tdd-bookshelf/tree/master/?fontsize=14\&hidenavigation=1\&theme=dark)

## 练习目标

### 任务分解（API Tasking）

* [ ] 根据项目剖析中的需求，拆分业务 Domain，想清楚 JSON API 的 endpoint 及其数据格式和类型。

![Simple Consumer](https://github.com/pact-foundation/pact-workshop-js/raw/step11/diagrams/workshop_step1.svg?sanitize=true)

举个例子，我们需要实现：

* `GET /api/books` - Retrieve all books (with query?)
* `GET /api/books/{id}` - Retrieve a single book detail by ID

![Sequence Diagram](https://github.com/pact-foundation/pact-workshop-js/raw/step11/diagrams/workshop_step1_class-sequence-diagram.svg?sanitize=true)

### 编写契约测试（Contract Testing）

* [ ] 编写契约测试，并生成契约文件，你将获得一个关于 HTTP Request 和 Response 的描述。

```bash
yarn pact:test #运行 pact 测试
```

![Generate Pact Contract File](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4J8z1jyJaZYEmaEjax%2F-M4J8zcuKNcOa1amMYuZ%2Fpact-file.svg?generation=1586254852405331\&alt=media)

```javascript
test('getting all books', async () => {
  // Given: set up Pact interactions
  await provider.addInteraction({
    state: 'books exist',
    uponReceiving: 'get all books',
    withRequest: {
      method: 'GET',
      path: '/api/books',
    },
    willRespondWith: {
      status: 200,
      headers: {
        'Content-Type': 'application/json; charset=utf-8',
      },
      body: like([somethingLike(bookContent), somethingLike(anotherBookContent)]),
    },
  })

  // When: make request to Pact mock server
  const response = await API.getAllBooks()

  // Then: verify the response data and status
  expect(response.data).toStrictEqual([bookContent, anotherBookContent])
  expect(response.status).toEqual(200)
})
```

### 启动 Mock Server

* [ ] 使用上述的契约文件通过 Pact 启动一个 [Mock Server](https://github.com/pact-foundation/pact-mock_service#stub-service-usage)

![Mock Pact Provider](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4J8z1jyJaZYEmaEjax%2F-M4J8zcwV-MsIZEU_Bij%2Fpact-provider.svg?generation=1586254851340108\&alt=media)

由于我们没有真正去实现一个 Provider 来提供 API，所以从前端的角度我们期望方便快捷得启动一个 Mocker Server。

```bash
yarn pact:stubs #启动 Pact Stub server
```

```javascript
const pact = require('@pact-foundation/pact-node')
const path = require('path')

const PACT_FILE = path.resolve(__dirname, '../tests/pacts/contracts/bookshelf-bookapi.json')

const server = pact.createStub({
  pactUrls: [PACT_FILE],
  port: 8080,
  cors: true,
})
server.start().then(function () {
  console.info(`Mock server running on http://localhost:${8080}/api`)
  // Do your testing/development here
})
```

在我们平常的前端开发当中也会用到 mock server，现在让我们把这个 mock server 改造一下，引入测试先行的概念，变成即可以提供 mock 数据，又可以作为契约自动化验证后端 real API，这样一举两得了。

参考资料：

* [Local API stubs - Pact](https://docs.pact.io/getting_started/stubs)
* [kununu/pact-cli: A CLI Tool to start an API mock server via pact interaction files.](https://github.com/kununu/pact-cli)

## 本次任务的练习要求

* 理解契约测试当中的 Consumer 和 Provider，以及 contract 契约文件。
* 设计符合 REST 规范的 API，参考文章：[什么才是真正的 RESTful 架构？](https://blog.jimmylv.info/2015-11-11-what-is-really-rest/)
* 契约先行，通过测试生成 mock server 再进行前端页面的业务开发。

## 也请你思考一下

### 契约测试与其他测试的关系？

![Before contract tests](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4J8z1jyJaZYEmaEjax%2F-M4J8zcyjzqydOMIXs6r%2Fbefore%20contract%20tests.jpg?generation=1586254849417207\&alt=media)

也许你也能够明白这一点，契约测试只是替代了部分的系统集成测试（即确保正确使用 API 以及 API 以期望的方式进行响应)。但是它并[不会取代其他维度的测试](https://docs.pact.io/faq#do-i-still-need-end-to-end-tests)，金字塔结构中的其他测试侧重点不同，保障的是应用程序的其他核心业务逻辑能够正常工作。

![After contract tests](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4J8z1jyJaZYEmaEjax%2F-M4J8zd-8ns_V3FsNupk%2Fafter%20contract%20tests.jpg?generation=1586254847324326\&alt=media)

那么，请你从契约测试的运行速度的角度思考一下，这个 Pact 测试跑起来快嘛？API 测试跟单元测试的运行速度有什么区别？分别有什么目的和侧重点？适用于什么样的场景？

下一步，如果你需要从软件开发的全局上下游考虑问题，你该如何向后端 API 开发人员（如果你们采取前后端分离的开发模式）解释契约测试呢？如何让双方理解前后端契约的价值并在项目中推行呢？

![Pact Broker](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4J8z1jyJaZYEmaEjax%2F-M4J8zd1PcNODrnWWKlh%2Fpact%20broker.svg?generation=1586254853340460\&alt=media)

尝试了解一下 [Pact Broker](https://docs.pact.io/pact_broker)，有了它之后，后端如果再悄咪咪改 API 字段，公共的 Pact Broker 瞬间就晓得了，契约这一层属于中间层，有点中间人的意思，当然它还可以映射各服务之间的调用关系。

使用 Pact Stub server 的办法直接开启前端开发的进程，摆脱对后端 API 环境未 Ready 的依赖。那么更进一步，是时候将契约测试推广到后端，跟后端开发小伙伴共同约定一份契约，放到 Pact Broker 以及 CI 用来辅助 API 开发吧！

![测试覆盖范围对比](https://insights.thoughtworks.cn/wp-content/uploads/2018/05/9.png)

### 在 E2E 层面如何使用 Mock Server？

其实 Cypress 也可以 [Stubbing the server](https://docs.cypress.io/guides/getting-started/testing-your-app.html#Stubbing-the-server)，但是我建议不要在 E2E **框架**层面 mock 任何东西，要防止因为 API 层面挂掉，而导致前端无法继续开发，或者持续集成无法进行。

```javascript
cy.server()

cy.fixture('books.json').as('booksJSON')
cy.route('GET', 'books/*', '@booksJSON')
```

但是，我们要尽可能让前端的 mock server 跟后端的 real server 保持一致，这样在每次后端（Provider） API 变更时都能够**测试**是否满足前端的需要，又能够在前端（Consumer）的角度**检查** API 是否破坏了既有需求，从而保障约定好的契约与实际实现能够正确执行。

理解一下 Functional Testing vs Integration E2E Testing 的区别，前者其实是可以在前端利用 mock server 保障前端的功能不会挂，即 API 数据供给永远都是好的，我们测试的是前端的数据显示与交互功能是否正常如初；而后者则更偏向于端到端的功能集成测试，当整个应用出现错误异常，有可能是前端出现问题，有可能是后端 API 出现问题，也有可能是前后端各自安好，但是对接出现问题。

这时，出现 Bug 时我们就要去定位问题，而我们会发现，问题发现得越晚，其修复成本越高。我们得去逐一排查：前端界面 -> 前端数据流（Store） -> 前后端集成 -> 后端 API 数据 -> 后端数据库，然后去是不是前端有问题，如果前端没问题那就再切换视角到后端，而如果前后端当前的优先级不一致，就会导致各自排查的积极性不足，而导致前端等后端，后端怪前端的尴尬境地。测试驱动开发，让我们可以从契约测试出发，先定义好契约然后用自动化的方式去保障和追踪 API 接口变更是否满足预期，对比一下，再想想你们做前后端联调时的痛苦吧！

![API 少了个字段](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M2gGyOW9eIymnNBDuu0%2F-M2gGz7YqeVg9reqHvDM%2Fapi-missing-a-field.png?generation=1584512104231367\&alt=media)

### 从协同的角度来说，GraphQL 的价值是什么？

> [GraphQL | 一种为你的 API 而生的查询语言](https://graphql.cn/)

![GraphQL vs REST](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4J8z1jyJaZYEmaEjax%2F-M4J8zd3VFvNpuOFyBX3%2Fgraphql%20vs%20rest.jpg?generation=1586254848355198\&alt=media)

![GraphQL](https://2897586075-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-LquCgGNObzQ8HY5Og1P%2F-M4J8z1jyJaZYEmaEjax%2F-M4J8zd5ROgkdmthSNjg%2Fgraphql.png?generation=1586254850861205\&alt=media)

参考资料：

* [Schema-First GraphQL: The Road Less Travelled - Blog | Mirumee](https://blog.mirumee.com/schema-first-graphql-the-road-less-travelled-cf0e50d5ccff)
* [Code-first vs. schema-first development in GraphQL - LogRocket Blog](https://blog.logrocket.com/code-first-vs-schema-first-development-graphql/)

### 阅读材料

* [聊一聊契约测试](https://insights.thoughtworks.cn/about-contract-test/)
* [Consumer-Driven Contracts: A Service Evolution Pattern](https://martinfowler.com/articles/consumerDrivenContracts.html)
* [pact-foundation/pact-workshop-js: Pact JS workshop - learn Pact in 60 minutes](https://github.com/pact-foundation/pact-workshop-js)
