实践中的消费者驱动契约测试

📝 面试求职: 「面试试题小程序」 ,内容涵盖 测试基础、Linux操作系统、MySQL数据库、Web功能测试、接口测试、APPium移动端测试、Python知识、Selenium自动化测试相关、性能测试、性能测试、计算机网络知识、Jmeter、HR面试,命中率杠杠的。(大家刷起来…)

📝 职场经验干货:

软件测试工程师简历上如何编写个人信息(一周8个面试)

软件测试工程师简历上如何编写专业技能(一周8个面试)

软件测试工程师简历上如何编写项目经验(一周8个面试)

软件测试工程师简历上如何编写个人荣誉(一周8个面试)

软件测试行情分享(这些都不了解就别贸然冲了.)

软件测试面试重点,搞清楚这些轻松拿到年薪30W+

软件测试面试刷题小程序免费使用(永久使用)


在上一篇我们已经介绍了消费者驱动契约测试。此时此刻,我确信你已经迫不及待地想要开始实际的实施了。

那么,我们不再耽搁,马上开始!

我们先从使用 Pact 进行实现开始。根据官方文档,Pact 是一个以代码为先的工具,用于通过契约测试测试 HTTP 和消息集成。

我们将使用用 JavaScript 编写的消费者 - 提供者应用程序作为测试系统。你可以在 GitHub 仓库中找到源代码。

消费者测试

消费者测试的重点是检查消费者的期望是否与提供者的行为相匹配

这些测试并不是为了验证提供者的任何功能,而是专注于消费者的需求,并验证这些期望是否得到满足。

松散匹配器

为了避免测试变得脆弱且不可靠,使用松散匹配器是一个最佳实践。这使得契约测试对提供者响应中的微小变化更具弹性。

通常,只要数据类型匹配,提供者在验证期间返回的确切值并不重要(Pact 文档)。然而,当需要验证响应中的特定值时,可以例外。

Pact 提供了几种匹配器,允许通过验证数据类型和结构而不是确切值来进行灵活的契约测试。关键的松散匹配器可以在 Pact 文档中找到。

没有松散匹配器的示例(严格匹配):

describe("getBook", () => {
    test("returns a book when a valid book id is provided", async () => {

        await provider.addInteraction({
            states: [{ description: "A book with ID 1 exists" }],
            uponReceiving: "a request for book 1",
            withRequest: {
                method: "GET",
                path: "/books/1",
            },
            willRespondWith: {
                status: 200,
                headers: { "Content-Type": "application/json" },
                body: {
                    id: 1,
                    title: "To Kill a Mockingbird",
                    author: "Harper Lee",
                    isbn: "9780446310789"
                },
            },
        })

        await provider.executeTest(async (mockService) => {
            const client = new LibraryClient(mockService.url)
            const book = await client.getBook(1)
            expect(book).toEqual(expectedBook)
        })
    })
})

问题:

如果 id、title、author 或 isbn 稍有变化,这个测试就会失败。

使用松散匹配器的示例(灵活且可维护):

使用 Pact 匹配器,我们允许提供者返回任何预期类型的合法值:​​​​​​​

describe("getBook", () => {
    test("returns a book when a valid book id is provided", async () => {
      const expectedBook = { id: 1, title: "To Kill a Mockingbird", author: "Harper Lee", isbn: "9780446310789" }

      await provider.addInteraction({
        states: [{ description: "A book with ID 1 exists" }],
        uponReceiving: "a request for book 1",
        withRequest: {
          method: "GET",
          path: "/books/1",
        },
        willRespondWith: {
          status: 200,
          headers: { "Content-Type": "application/json" },
          body: like(expectedBook),
        },
      })

      await provider.executeTest(async (mockService) => {
        const client = new LibraryClient(mockService.url)
        const book = await client.getBook(1)
        expect(book).toEqual(expectedBook)
      })
    })
  })

在这种情况下,即使实际值发生变化,契约仍然有效,验证仅关注确保数据类型和格式正确。

编写消费者契约测试的步骤

场景:

  • 验证 LibraryClient.getAllBooks() 是否检索到书籍列表。

  • 验证 LibraryClient.getBook(id) 在给定有效 ID 时是否正确获取单本书籍。

要开始动手实践,你需要克隆包含消费者和提供者的仓库。

要开始消费者测试,打开 consumer.js 文件。在其中,你可以找到 LibraryClient 类,它在消费者驱动契约测试设置中代表消费者。

它充当与外部图书馆 API(提供者)交互以获取和管理书籍数据的客户端。

其中有一些函数:

  • getBook(id) — 按其 ID 获取单本书籍。以 JSON 格式返回数据。

  • getAllBooks() — 从 API 获取所有书籍。以 JSON 格式返回书籍列表。

  • addBook(title, author, isbn) — 发送 POST 请求以添加一本新书。返回新创建的书籍的详细信息。

1.编写第一个消费者契约测试:

导入所需的依赖项和消费者类

const path = require('path');
const { PactV3, MatchersV3 } = require('@pact-foundation/pact');
const LibraryClient = require('../src/client');

2. 设置模拟提供者​​​​​​​

const provider = new PactV3({
    dir: path.resolve(process.cwd(), 'pacts'),
    consumer: "LibraryConsumer",
    provider: "LibraryProvider"
})

上面的代码使用 PactV3 库创建了一个 Pact 模拟提供者(提供者),其中指定:

  • LibraryConsumer 作为消费者(发出请求的客户端)的名称。

  • LibraryProvider 作为提供者(响应请求的 API)的名称。

  • 传递参数 dir 以定义存储契约的目录

3. 设置消费者和模拟提供者的交互,并注册消费者期望。​​​​​​​

const EXPECTED_BOOK = { id: 1, title: "To Kill a Mockingbird", author: "Harper Lee", isbn: "9780446310789" }

describe("getAllBooks", () => {
    test("returns all books", async () => {

        provider
            .uponReceiving("a request for all books")
            .withRequest({
                method: "GET",
                path: "/books",
            })
            .willRespondWith({
                status: 200,
                body: MatchersV3.eachLike(EXPECTED_BOOK),
            })

        await provider.executeTest(async (mockService) => {
            const client = new LibraryClient(mockService.url)
            const books = await client.getAllBooks()
            expect(books[0]).toEqual(EXPECTED_BOOK)
        })
    })
})

describe("getBook", () => {
    test("returns a book when a valid book id is provided", async () => {

        provider
            .given('A book with ID 1 exists')
            .uponReceiving("a request for book 1")
            .withRequest({
                method: "GET",
                path: "/books/1",
            })
            .willRespondWith({
                status: 200,
                body: MatchersV3.like(EXPECTED_BOOK),
            }),

        await provider.executeTest(async mockProvider => {
            const libraryClient = new LibraryClient(mockProvider.url)
            const book = await libraryClient.getBook(1);
            expect(book).toEqual(EXPECTED_BOOK);
        })
    })
})

首先,我们定义了预期的书籍。这个对象代表我们期望 API 返回的单本书籍。它充当书籍响应应该是什么样子的模板。

  • provider.addInteraction({...}) 设置了一个模拟交互。

  • uponReceiving:描述测试期望的内容。

  • withRequest:定义预期请求的详细信息:

      • 方法:GET

      • 端点:/books

  • willRespondWith:定义预期响应:

      • 状态码:200

      • 正文:MatchersV3.eachLike(EXPECTED_BOOK)

      • eachLike(EXPECTED_BOOK):确保响应包含一个对象数组,这些对象与 EXPECTED_BOOK 的结构匹配。

4. 调用消费者针对模拟提供者:​​​​​​​

        await provider.executeTest(async mockProvider => {
            const libraryClient = new LibraryClient(mockProvider.url)
            const book = await libraryClient.getBook(1);
            expect(book).toEqual(EXPECTED_BOOK);
        })

现在,你已经准备好运行测试了!首先,在我们的 package.json 文件中创建一个名为 test:consumer 的新脚本,它使用 jest 命令后跟你要执行的测试文件:

"test:consumer": "jest consumer/test/consumer.test.js",

保存更改并运行测试,执行以下命令:

npm run test:consumer

如果一切设置正确,你应该会看到一个测试通过:

如果测试通过,就会生成一个契约并保存在 pacts 文件夹中。如果测试失败,则无法创建契约。

契约的内容应包括有关消费者、提供者、已设置的交互、提供者预期的请求和响应详细信息、匹配规则以及任何其他相关信息。​​​​​​​

{
  "consumer": {
    "name": "LibraryConsumer"
  },
  "interactions": [
    {
      "description": "a request for all books",
      "request": {
        "method": "GET",
        "path": "/books"
      },
      "response": {
        "body": [
          {
            "author": "Harper Lee",
            "id": 1,
            "isbn": "9780446310789",
            "title": "To Kill a Mockingbird"
          }
        ],
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type",
                  "min": 1
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "a request for book 1",
      "providerStates": [
        {
          "name": "A book with ID 1 exists"
        }
      ],
      "request": {
        "method": "GET",
        "path": "/books/1"
      },
      "response": {
        "body": {
          "author": "Harper Lee",
          "id": 1,
          "isbn": "9780446310789",
          "title": "To Kill a Mockingbird"
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    }
  ],
  "metadata": {
    "pact-js": {
      "version": "11.0.2"
    },
    "pactRust": {
      "ffi": "0.4.0",
      "models": "1.0.4"
    },
    "pactSpecification": {
      "version": "3.0.0"
    }
  },
  "provider": {
    "name": "LibraryProvider"
  }
}

提供者测试

提供者契约测试的主要目标是验证消费者生成的契约。Pact 提供了一个框架,用于检索此契约并重放所有已注册的消费者交互,以确保符合要求。测试是对真实服务运行的。

提供者状态

在编写提供者测试之前,我想介绍另一个有用的概念:提供者状态。

遵循最佳实践,交互应该独立验证,因此为每个测试用例独立维护上下文至关重要。提供者状态允许你在交互运行之前直接将数据注入到提供者的数据源中,从而设置提供者上的数据。这确保提供者生成的响应与消费者的期望一致。

提供者状态的名称在消费者端的 given 子句中定义。然后使用此名称在提供者中找到对应的设置代码,确保正确放置了数据。

示例:

考虑测试用例:“存在 ID 为 1 的书籍。

为了确保存在必要的数据,我们在 stateHandlers 中定义一个提供者状态,指定消费者 given 子句中的名称:​​​​​​​

            stateHandlers: {
                "A book with ID 1 exists": () => {
                    return Promise.resolve("Book with ID 1 exists")
                },
            },

在消费者端,提供者状态在 given 子句中引用:​​​​​​​

        provider
            .given('A book with ID 1 exists')
            .uponReceiving("a request for book 1")
            .withRequest({
                method: "GET",
                path: "/books/1",
            })
            .willRespondWith({
                status: 200,
                body: MatchersV3.like(EXPECTED_BOOK),
            }),

1.这种设置确保在交互运行之前,提供者拥有必要的数据,从而能够向消费者返回预期的响应。

编写提供者测试

导入所需的依赖项

const { Verifier } = require('@pact-foundation/pact');
const app = require("../src/server.js");

2. 运行提供者服务

const server = app.listen(3000)

3. 设置提供者选项​​​​​​​

        const opts = {
            provider: "LibraryProvider",
            providerBaseUrl: "http://localhost:3000",
            publishVerificationResult: true,
            providerVersion: "1.0.0",
        }

4. 编写提供者契约测试

在设置好提供者验证器选项后,让我们使用 Jest 框架编写实际的提供者契约测试。​​​​​​​

        const verifier = new Verifier(opts);

        return verifier
            .verifyProvider()
            .then(output => {
                console.log('Pact Verification Complete!');
                console.log('Result:', output);
            })

5. 运行提供者契约测试

在运行测试之前,你需要在 package.json 文件中创建一个名为 test:provider 的新脚本,它使用 jest 命令后跟你要执行的测试文件:

"test:provider": "jest provider/test/provider.spec.js"

保存更改并运行测试,执行以下命令:

npm run test:provider

如果一切设置正确,你应该会看到一个测试通过:

结论

今天,我们探索了消费者驱动契约测试方法的实用实现。我们为消费者和提供者都创建了测试用例,并将契约存储在同一个仓库中。

但你可能会想——如果消费者和提供者的仓库是分开的,与我们的情况不同呢?由于这两个微服务是独立的,契约需要对两者都可访问。那么,它应该存储在哪里呢?

让我们在下一部分中探讨可能的解决方案。

最后: 下方这份完整的软件测试视频教程已经整理上传完成,需要的朋友们可以自行领取【保证100%免费】

​​

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值