与Joi进行API合同测试

90 阅读12分钟

当你签署一份合同时,你希望双方都能坚持他们的交易。对于测试应用程序也是如此。合同测试是一种确保服务之间可以相互通信的方式,而且服务之间共享的数据与一组指定的规则是一致的。在这篇文章中,我将指导你使用Joi作为一个库,为消费API的服务创建API合同。

本教程包括。

  • 设置Joi合同测试框架
  • 使用Joi为我们的API端点编写合同测试
  • 使用Joi来验证API响应
  • 编写一个测试,根据模式验证API响应

前提条件

为了更好地跟上进度,你需要具备一些条件。

  1. 掌握JavaScript的基本知识
  2. 编写测试的基本知识
  3. 在您的系统上安装Node.js(版本>=11.0)
  4. 一个CircleCI账户
  5. 一个GitHub账户

在这篇文章中,我将演示为一个开源的API端点编写合同测试。我们要测试的端点是监控不同货币的比特币价格指数。然后我们将使用一个NodeJS应用程序来测试Coinbase API提供的返回响应的合约。

克隆演示项目

要开始练习,你将需要克隆演示项目。该项目是一个建立在Express.js 上的Node应用。Coinbuzz应用本身还没有开发。在本教程中,我们将专注于合同测试,这将有助于确保应用程序的稳定性,就像它将被开发一样。

该项目将使用Coinbase APIs,对于本教程,我们将使用端点https://api.coindesk.com/v1/bpi/currentprice/CNY.json 。这个端点获取了人民币对美元的比特币价格指数。

通过运行这个命令克隆项目。

git clone git@github.com:CIRCLECI-GWP/coinbuzz-contract-testing.git

首先通过导航到项目文件夹来安装依赖项。

cd coinbuzz-contract-testing;

npm install

没有必要运行应用程序,因为我们只在这个项目中运行我们的测试。

为什么使用合约测试

在我们开始之前,让我给你一些关于合同测试的背景。这种测试提供了不同服务在需要时工作的信心。想象一下,一个组织有多个支付服务,利用一个Authentication API。该API用一个username 和一个password 将用户登录到一个应用程序中。然后,当登录操作成功时,它为他们分配一个access token 。其他服务,如LoansRepayments ,一旦用户登录,就需要使用Authentication API服务。

Microservices Dependencies

如果Authentication 服务改变了它的工作方式,需要email 而不是username ,那么LoansRepayments 服务就会失败。通过契约测试,LoansRepayments 服务都可以通过对来自服务的请求的预期行为集来跟踪Authentication API。服务可以获得关于失败发生的时间、方式和地点的信息。还有关于故障是否由外部依赖引起的信息,在这种情况下是Authentication

设置合同测试环境

契约测试的目的是监测应用程序的状态,并在出现意外结果时通知测试人员。当合约测试被一个依赖其他服务稳定性的工具使用时,它是最有效的。有两个例子。

  • 一个依赖后端 API 稳定性的前端应用程序(如我们的项目)。
  • 一个微服务环境或一个依赖另一个API来处理信息的API

测试CoinDesk的端点将帮助我们了解不同货币在不同日子的比特币价格指数。要了解更多,请在我们的克隆应用程序中查看app.js

在你开始测试API之前,你需要了解其结构。这些知识将帮助你编写你的合同。首先,使用浏览器对这个URL做一个简单的GET 请求。

https://api.coindesk.com/v1/bpi/currentprice/CNY.json

这个请求获取了一定时期内比特币的当前价格。然后,它显示了美元(United States Dollars)和人民币(CNY)的转换情况。

Bitcoin Price Index browser response

虽然响应对象一开始可能看起来有点吓人,但我们可以通过使用Joi ,将其分解为单个对象来解决这个问题,我们将在本教程的下一节进行。但在这之前,让我们首先设置我们的CircleCI管道。

在CircleCI上设置一个项目

如果你克隆了样本项目库,它已经在git中被初始化和设置了。这对了解我们的项目如何与CircleCI集成是有帮助的。要设置CircleCI,通过运行命令在你的项目中初始化一个GitHub仓库。

git init

接下来,在根目录下创建一个.gitignore 文件。在该文件中,添加node_modules ,以忽略npm生成的模块被添加到你的远程仓库。添加一个提交,然后推送你的项目到GitHub

登录CircleCI,浏览Projects 。所有与你的GitHub用户名或组织相关的仓库都会被列出,包括你要在CircleCI中设置的仓库。在这种情况下,它是coinbuzz-contract-testing

adding-a-project

在项目仪表板上,点击设置项目按钮。然后,点击使用现有的配置

start-building-page

当有提示时,点击开始构建。该管道失败了,这是预料之中的事。我们仍然需要在GitHub上添加我们定制的.circleci/config.yml 配置文件,才能正常构建项目。

start-building-prompt

编写CI管线配置

一旦我们建立了CircleCI管道,现在是时候将CircleCI添加到我们的本地项目中。首先,在根目录下创建一个名为.circleci 的文件夹。在该文件夹中,创建一个config.yml 文件。现在添加配置细节。

version: 2.1
jobs:
  build:
    working_directory: ~/repo
    docker:
      - image: cimg/node:10.16.3
    steps:
      - checkout
      - run:
          name: update npm
          command: "npm install -g npm@5"
      - restore_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
      - run:
          name: install dependencies
          command: npm install
      - save_cache:
          key: dependency-cache-{{ checksum "package-lock.json" }}
          paths:
            - ./node_modules
      - run:
          name: run coinbuzz contract-tests
          command: npm test
      - store_artifacts:
          path: ~/repo/coinbuzz

在这个配置中,CircleCI使用一个Node Docker镜像,从环境中提取。然后,它更新npm包管理器。接下来,如果缓存存在,它将被恢复。只有当用save-cache 检测到变化时,才会更新应用程序的依赖关系。运行Coinbuzz测试,缓存的项目被存储在artifactscoinbuzz 目录中。

推送你的修改到GitHub,CircleCI会自动开始构建过程。因为我们还没有任何测试,我们的管道将失败(再次)。这没关系,因为我们将在添加测试后重新运行。即使没有测试,管道的细节也是可以审查的。

创建Joi合同

Joi是一个工具,它可以分析对象并将其分解成可以验证的块。Joi把一个响应看作是一个单一的对象,有两个关键值:timebpi 。每个值都是一个特定的数据类型。通过分解响应,Joi可以分析响应并根据定义的模式合同创建成功或失败的断言。

要为我们的项目安装joi 包,打开一个终端窗口并运行这个命令。

npm install joi

很好!我们已经开始了创建我们的Joi合同的过程。本节将介绍如何只创建对象的一部分,然后可以扩展到创建响应的其他部分。我们将使用一个示例代码块,展示如何将对象分解成几块。我们可以测试currencyObject ,以后可以对美元或人民币重新使用。

const currencyObject = Joi.object({
        code: Joi.string().required(),
        symbol: Joi.string().optional(),
        rate: Joi.string().required(),
        description: Joi.string().required(),
        rate_float: Joi.number().required()
  }).required();

在这个代码块中,我们正在拆分货币对象,并告诉Joi,在我们的响应中,我们期望货币对象有关键项code,symbol,rate,description, 和rate_float 。同样在合同中,描述了我们对象中项目的状态,包括它们是否应该是optional()required() 。在这种情况下,我们将symbol 关键项添加为可选项,因为我们希望在其他具有类似结构的响应中重复使用currencyObject

Partial API response and JOI contract

货币对象与美元和人民币的货币响应都是并排匹配的。它可以被重复使用,因为currencyObject 合同与人民币和美元的响应对象的标准相匹配。完整的BPIContractcurrencyObject 集成,可以多次重复使用。响应的完整合约的更大背景是来自CoinDesk的CNY请求

在这一节中,我们已经涵盖了创建Joi合约和定义需要由合约模式验证的响应的属性。请记住,为了 "收紧 "模式,你应该定义合同模式的值是required 还是optional 。这就为在失败时抛出错误还是忽略它创造了一个界限。

处理Joi合同错误

在编写合同时,处理因响应的问题而可能产生的错误是很重要的。你需要知道每次执行测试时,响应是否一致,以及当合同模式出现故障时。了解潜在的合同失败将有助于你学习如何调整你的应用程序来处理失败。

为了处理这些错误,创建一个目录并将其称为lib 。在该目录中添加两个文件。

  1. 一个是请求帮助器,帮助使用API请求。axios
  2. 另一个文件用于schemaValidation() 方法,根据定义的模式验证响应

side by side lib files

模式验证文件验证所提供的响应和合同是否一致。如果它们不一致,就会导致一个错误。下一个代码块显示了我们正在使用一个方法,根据定义的Joi合同模式验证收到的响应。

 async function schemaValidation(response, schema) {
    if (!response || !schema) throw new Error('An API response and contract are required');
    const options = { abortEarly: false };
    try {
        const value = await schema.validateAsync(response, options);
        return value
    }
    catch (err) {
        throw new Error(err);
    }
}

module.exports = {
    schemaValidation
}

schema.validateAsync() Joi方法负责根据创建的模式验证响应。在这种情况下,我们已经在文件contracts/bpi-contracts.js 中创建了一个模式。在下一节中,我们将通过编写一个测试并传入模式和从Coinbase API收到的API响应来验证这个方法是否有效。

编写测试和断言

你已经成功地编写了你的合约和一个方法来验证它们与API响应,同时检查不一致的地方。我们的下一步是编写一个使用该方法的测试。为了使用我们的模式测试API,你需要安装jest,一个JavaScript测试框架,和axios,一个JavaScript请求制作框架。

npm install jest axios

我们将使用Jest来运行我们的测试,并使用axios来向Coinbase的端点发出API请求。为了实现这一点,在package.json 文件中添加test 命令来运行测试。

"scripts": {
    "start": "node ./bin/www",
    "test": "jest"
  },

在脚本部分添加该命令使Jest能够扫描我们的项目,以寻找任何具有.spec.test 扩展名的文件。当发现一个文件时,Jest将其视为测试文件并运行它们。

现在,在contract-tests 目录中创建一个测试。这个测试在发出API请求后调用模式验证方法。

const { getData } = require('../lib/request-helper')
const { schemaValidation } = require('../lib/validateContractSchema');
const { BPIContract } = require('../contracts/bpi-contracts');

describe('BPI contracts', () => {
    test('CNY bpi contract schema check', async () => {
        const response = await getData({
            url: 'https://api.coindesk.com/v1/bpi/currentprice/CNY.json' });
        return schemaValidation(response, BPIContract);
    });
});

这个调用使用getData() 方法来进行请求。getData() 方法使用axios调用Coinbase的API,然后使用响应来验证API响应与模式验证方法schemaValidation() 。这已经有我们的Joi模式定义,用于从我们的端点收到的响应。运行我们的测试,并通过运行来验证它是否有效。

npm test 

检查你的终端,看看是否有好消息。

successful test run

Voila!我们的测试通过了。

不过,请稍等。现在举办聚会还为时过早。我们需要验证该测试是否也能处理合同正确的情况。在以前的合同模式中,我们在currencyObject 中使用了字段symbol: Joi.string().optional() 。我们可以将Joi的期望值改为required() ,并找出Joi是否会产生错误。编辑currencyObject ,使之与之匹配。

const currencyObject = Joi.object({
        code: Joi.string().required(),
        symbol: Joi.string().required(),
        rate: Joi.string().required(),
        description: Joi.string().required(),
        rate_float: Joi.number().required()
    }).required();

在重新运行我们的测试后,我们确实出现了一个错误。

failed test run

Joi能够捕捉到这个错误,现在期望symbol 对象项是required() ,而不是货币对象中的optional() 。来自 API 的响应没有symbol 作为响应的一部分,Joi 拒绝了 API 的响应。该响应不符合合同的标准。

我们现在可以宣布这是一个成功。我们已经能够证明我们的合同模式是有效的。对响应的修改将导致失败,我们将被提醒。

验证管道的成功

现在,测试工作已经完成,CI管道也已建立,你可以将所有文件添加到git中,并推送到Github远程仓库。我们的CircleCI管道应该启动并自动运行我们的测试。要观察管道的执行情况,进入CircleCI仪表板,点击项目名称(coinbuzz-contract-testing)。

要查看构建状态,从CircleCI仪表板上选择构建。你将能够查看你的CircleCI配置文件中定义的每个步骤的状态。

build-status

一切都显示为绿色:胜利

总结

在本教程中,你已经为API响应创建了一个合同模式,一个处理模式中错误的方法,以及一个验证合同模式工作的测试。我希望能够证明,为依赖于外部服务可用性的API或前端应用程序设置合同测试是多么容易。当你能把一个失败的服务精确到一个改变了的响应对象时,就有可能忘记未检测到的依赖性松散性。


Waweru Mwaura是一名软件工程师和终身学习者,专门从事质量工程。他是packt的一名作者,喜欢阅读有关工程、金融和技术的文章。你可以在他的网页上阅读更多关于他的信息。