任务 1:练习 API 契约测试

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

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

什么是契约测试?

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

Pact 测试又名契约测试,是消费者服务与生产者服务之间的消费者驱动测试,消费者驱动的契约测试(Consumer-Driven Contracts)。

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

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/

练习目标

任务分解(API Tasking)

举个例子,我们需要实现:

  • GET /api/books - Retrieve all books (with query?)

  • GET /api/books/{id} - Retrieve a single book detail by ID

编写契约测试(Contract Testing)

yarn pact:test #运行 pact 测试
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

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

yarn pact:stubs #启动 Pact Stub server
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,这样一举两得了。

参考资料:

本次任务的练习要求

  • 理解契约测试当中的 Consumer 和 Provider,以及 contract 契约文件。

  • 设计符合 REST 规范的 API,参考文章:什么才是真正的 RESTful 架构?

  • 契约先行,通过测试生成 mock server 再进行前端页面的业务开发。

也请你思考一下

契约测试与其他测试的关系?

也许你也能够明白这一点,契约测试只是替代了部分的系统集成测试(即确保正确使用 API 以及 API 以期望的方式进行响应)。但是它并不会取代其他维度的测试,金字塔结构中的其他测试侧重点不同,保障的是应用程序的其他核心业务逻辑能够正常工作。

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

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

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

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

在 E2E 层面如何使用 Mock Server?

其实 Cypress 也可以 Stubbing the server,但是我建议不要在 E2E 框架层面 mock 任何东西,要防止因为 API 层面挂掉,而导致前端无法继续开发,或者持续集成无法进行。

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 接口变更是否满足预期,对比一下,再想想你们做前后端联调时的痛苦吧!

从协同的角度来说,GraphQL 的价值是什么?

GraphQL | 一种为你的 API 而生的查询语言

参考资料:

阅读材料

最后更新于