通过合同测试节省时间

2025-06-10

通过合同测试节省时间

大家好!今天的主题是契约驱动测试。

我们利用时间的方式很重要。
它对我们的生活很重要,对我们工作的公司也很重要。
我相信,为了我们的最佳利益,我们应该尽量减少投入到无效活动上的时间,最大限度地利用我们用于构建新想法的时间。

好吧,这很普遍,甚至有人会说这只是常识。但这和契约测试有什么关系呢?

在这里,我想说的是,在很多情况下,我们可以编写单元测试,而不是编写端到端测试或进行手动测试。
我们可以而且应该通过将不同组件的集成作为单元测试套件的一部分进行测试,来加快反馈循环(建立对所编写代码实际按预期运行的信心所需的时间)。

合同测试101

当两个实体进行通信时,供应商 API 的更改可能会导致其所有消费者出现故障。

当我讨论健壮性原则时,我已经部分触及了这个话题,事实上我认为契约测试是一种在不放弃开发速度的情况下使我们的服务更加健壮的方法。

我们该怎么办?我们编写集成测试/端到端测试/手动测试。
毫无疑问,这些测试有助于在生产环境结束之前发现错误,但也有缺点。

  1. 运行它们需要设置基础设施、部署应用程序等。它们比单元测试慢得多,因为这是我们调用其他服务、进行网络调用和使用数据库的地方。由于我们知道它们很慢并且需要所有东西都准备好,所以我们不能像通常的单元测试那样频繁地运行它们。
  2. 第一点的含义是它们增加了开发反馈循环。
  3. 由于我们与其他开发人员共享同一条流水线,因此集成测试失败并不一定意味着流水线被破坏了。因此,我们还需要更多时间来调查发生了什么。

在研究合同测试的具体示例之前,让我们先看一下测试金字塔。

测试金字塔

金字塔以图形方式展示了我们应该对每种类型的测试进行多少次。底部是单元测试,这意味着我们应该按比例编写更多的单元测试:

  • 它们确保我们的代码在独立运行时能够正确运行
  • 它们易于编写且运行速度快

我们编写了很多这样的程序,每次对代码库进行代码更改或更新某个依赖项时都会执行它们。

根据我所说的集成测试和端到端测试,它们被置于金字塔的顶端并不奇怪。

例子

让我们看一个使用契约测试而不是端到端测试的具体示例。

示例 1

Context:客户端与服务端通信。
Scenario:一个 ReactJs 应用,用于管理用户的 ToDo 列表。ToDo 列表被序列化并发送到服务器,服务器将信息存储到 S3 中。
What we want to test:代码的任何更改不会导致系统回归,也就是说,我们仍然能够反序列化从服务器接收的 ToDo 列表,并将其显示在 React 组件中。

待办事项列表可能如下所示:

export class TodoList {
    items: Item[]

    constructor(items: Item[] = []) {
        this.items = items;
    }
}

// ......
// And this an item of our TodoList

export class Item {
    constructor(public description: string = '', 
                public isChecked: boolean = false) {}
}
Enter fullscreen mode Exit fullscreen mode

在代码中的某个地方,我们发出 http 请求来获取 TodoList,对其进行反序列化并更新视图的状态。

方法 1(不好)

我们可以编写一个端到端测试:

  • 打开浏览器(使用量角器、硒或类似工具)
  • 请求 React 应用程序
  • 将一些项目添加到待办事项列表中
  • 保存待办事项列表
  • 再次获取 ToDo 列表
  • 断言 ToDo 小部件中的信息显示正确,并且没有发生其他错误。

这正是我们想要避免写的东西;它既慢又脆弱。
我们可以通过使用契约测试来避免端到端测试。

方法 2(良好)

首先,让我们为待办事项列表创建一个合约。
我们将使用的工具如下:

  • Jest 用于单元测试(任何其他单元测试工具都可以正常工作)
  • Typescript-json-schema将我们的待办事项列表转换为 Json Schema
  • Json 模式验证器用于测试我们的 ToDo 列表是否符合合同

让我们定义一个实用函数,该函数在我们第一次运行测试时创建合约:

getOrCreateContract = (instance, filename) => {
    if (schemaDoesNotExist(filename)) {
        // TJS comes from the Typescript-json-schema lib
        const program = TJS.getProgramFromFiles([resolve(filename)], {}, basePath);
        const schema = TJS.generateSchema(program, instance, settings);
        saveSchema(CONTRACT_FOLDER, filename);

        return schema;
    }

    return getSchema(CONTRACT_FOLDER, filename);
};
Enter fullscreen mode Exit fullscreen mode

为我们的待办事项列表生成的合同如下所示:

{
    "$schema": "http://json-schema.org/draft-06/schema#",
    "definitions": {
    "Item": {
        "properties": {
            "description": {
                "default": "",
                    "type": "string"
            },
            "isChecked": {
                "default": false,
                    "type": "boolean"
            }
        },
        "type": "object"
    }
},
    "properties": {
    "items": {
        "items": {
            "$ref": "#/definitions/Item"
        },
        "type": "array"
    }
},
    "type": "object"
}
Enter fullscreen mode Exit fullscreen mode

现在,我们来编写契约测试:

describe('ToDo List', () => {
    test('respect contract', () => {
        let todo = new TodoList([
            new Item('contract tests', true)
        ]);

        let contract = getOrCreateContract(todo, 'TodoList.schema.json');
        let contractValidator = new Validator();
        let respectContract = () => {
            contractValidator.validate(todo, contract);
        };

        expect(respectContract().error().length).toBe(0);
    });
});

Enter fullscreen mode Exit fullscreen mode

这个测试给予我们与上述端到端测试完全相同的信心,但它的速度更快,并且不需要与真实的依赖关系进行通信。

显然,在某些情况下我们需要更新合约。例如,我们可以添加命令行参数来覆盖现有合约。

示例 2

Context:服务到服务通信

我觉得这篇文章有点太长了。服务间通信的后果确实需要介绍一些概念,所以我会在后续的文章中写出例子。

结论

集成测试在测试金字塔中占据重要地位,但有时我们会过度使用它们。
契约测试可以节省我们的时间!

如果你对这个话题感兴趣,请告诉我!
这能帮助我了解是否应该写一篇关于服务间沟通的后续文章。

谢谢!
Nicola

鏂囩珷鏉ユ簮锛�https://dev.to/napicella/contract-testing-p53
PREV
Golang 模式 - 第一部分
NEXT
#codeNewbie 需要开始/改进什么?