Contract Testing: Chia sẻ file contract dễ dàng với Pact Broker

Làm sao để chia sẻ file contract khi Consumer và Provider được phát triển bởi hai team độc lập? Đã có Pact Broker giúp dễ dàng chia sẻ file contract cũng như kết quả contract testing, đồng thời có khả năng kiểm tra tính tương thích giữa các version của các service với nhau.

Upload image

Trong bài viết tìm hiểu về Contract Testing, chúng ta đang dừng lại ở phần sử dụng Pact để hiện thực Contract Testing. Chúng ta đã thấy Pact giúp phát hiện được lỗi khi đổi trường email thành emails. Tuy nhiên, trong quá trình này, chúng ta thấy là Consumer và Provider phải sử dụng chung file Contract được tạo ra khi chạy pact test ở Consumer.

Upload image

Vậy khi Consumer và Provider được phát triển bởi hai team độc lập thì làm thế nào để file Contract này được chia sẻ và cập nhật giữa hai team?

Trường hợp 1: Consumer và Provider sử dụng chung một git repo, ta chỉ cần commit file Contract lên git repo.

Trường hợp 2: Consumer và Provider sử dụng git repo riêng, chúng ta có 2 cách giải quyết:

  1. Chia sẻ file thông qua URL (upload file lên S3, Blob Storage…)
  2. Sử dụng Pact Broker

Trong bài viết lần này, chúng ta sẽ tìm hiểu cách sử dụng và một số tính năng chính của Pact Broker. Điều đặc biệt là Pact Broker có thể hỗ trợ cho cả hai trường hợp bên trên.

Pact Broker là ứng dụng để chia sẻ contract và kết quả contract testing. Pact Broker được tối ưu hóa để tương thích với file contract được tạo ra từ Pact, tuy nhiên có thể được dùng với các file contract khác có định dạng JSON.

Để sử dụng Pact Broker, chúng ta có thể dùng PactFlow hoặc tự host Broker server sử dụng pactfoundation/pact-broker docker image. Trong bài viết này chúng ta sẽ tự host Broker server.

Chúng ta sẽ dùng file docker-compose sau để host Pact Broker:

version: '3'

services:

  postgres:
    image: postgres
    healthcheck:
      test: psql postgres --command "select 1" -U postgres
    ports:
      - "5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: postgres

  broker_app:
    image: pactfoundation/pact-broker
    ports:
      - "9292:9292"
    links:
      - postgres
    environment:
      PACT_BROKER_DATABASE_USERNAME: postgres
      PACT_BROKER_DATABASE_PASSWORD: password
      PACT_BROKER_DATABASE_HOST: postgres
      PACT_BROKER_DATABASE_NAME: postgres
      PACT_BROKER_LOG_LEVEL: DEBUG

Chạy lệnh docker compose up, truy cập vào localhost:9292:

Upload image

Tiếp theo, chúng ta update pact tests của Consumer để đưa contract lên Pact Broker:

// pkg/consumer/consumer_test.go
package consumer

import (
   "errors"
   "fmt"
   "github.com/pact-foundation/pact-go/dsl"
   "github.com/pact-foundation/pact-go/types"
   "testing"
)

func TestConsumer(t *testing.T) {
   // init Pack DSL
   pact := dsl.Pact{
      Consumer: "example-consumer",
      Provider: "example-provider",
   }

   // setup Pack mock server
   pact.Setup(true)

   t.Run("get user by id", func(t *testing.T) {
      id := "1"

      pact.
         AddInteraction().
         Given("User John exists").                 // specify Provider state
         UponReceiving("User 'John' is requested"). // specify test case name
         WithRequest(dsl.Request{                   // specify expected request
            Method: "GET",
            Path:   dsl.Term("/users/1", "/users/[0-9]+"), // specify matching for endpoint
         }).
         WillRespondWith(dsl.Response{ // specify expected response
            Status: 200,
            Body: dsl.Like(User{ // specify matching for response body
               ID:    id,
               Name:  "John",
               Email: "john@gmail.com",
            }),
         })

      // verify contract
      err := pact.Verify(func() error {
         // use pact mock server
         host := fmt.Sprintf("%s:%d", pact.Host, pact.Server.Port)

         // send request
         user, err := GetUserByID(host, id)
         if err != nil {
            return errors.New("error is not expected")
         }

         // check if actual response match expected
         if user == nil || user.ID != id {
            return fmt.Errorf("expected user with ID %s but got %v", id, user)
         }

         return err
      })

      if err != nil {
         t.Fatal(err)
      }
   })

   // Write Contract file
   if err := pact.WritePact(); err != nil {
      t.Fatal(err)
   }

   // specify PACT publisher
   publisher := dsl.Publisher{}
   err := publisher.Publish(types.PublishRequest{
      PactURLs:        []string{"./pacts/"},    // specify folder contains pact contract
      PactBroker:      "http://localhost:9292", // specify PACT broker URL
      ConsumerVersion: "1.0.0",
      Tags:            []string{"1.0.0", "latest"},
   })
   if err != nil {
      t.Fatal(err)
   }

   pact.Teardown()
}

Chạy pact tests, reload lại trang web, chúng ta có thể thấy contract đã được đưa lên Pact Broker:

Upload image

Upload image

Sau đó chúng ta update pact tests của Provider để lấy contract từ Pact Broker và đưa kết quả test lên Pact Broker:

// pkg/provider/provider_test.go
package provider

import (
   "github.com/pact-foundation/pact-go/dsl"
   "github.com/pact-foundation/pact-go/types"
   "testing"
)

func TestProvider(t *testing.T) {
   // init Pact DSl
   pact := dsl.Pact{
      Provider: "example-provider",
   }

   _, err := pact.VerifyProvider(t, types.VerifyRequest{
      BrokerURL:       "http://localhost:9292",
      ProviderBaseURL: "http://127.0.0.1:8080",
      ProviderVersion: "1.1.0",
      ConsumerVersionSelectors: []types.ConsumerVersionSelector{
         {
            Consumer: "example-consumer",
            Tag:      "1.0.0",
         },
      },
      PublishVerificationResults: true, // publish results of verification to PACT broker
   })

   if err != nil {
      t.Log(err)
   }
}

Chạy pact tests, reload lại trang web, chúng ta có thế thấy kết quả “success” màu xanh lá:

Upload image

Bây giờ chúng ta nâng version của Provider lên 1.1.0 và sửa trường email thành emails:

// pkg/provider/provider_test.go
...
_, err := pact.VerifyProvider(t, types.VerifyRequest{
   BrokerURL:       "http://localhost:9292",
   ProviderBaseURL: "http://127.0.0.1:8080",
   ProviderVersion: "1.1.0", // update version to 1.1.0
   ConsumerVersionSelectors: []types.ConsumerVersionSelector{
      {
         Consumer: "example-consumer",
         Tag:      "1.0.0",
      },
   },
   PublishVerificationResults: true, // publish results of verification to PACT broker
})

...

Chạy lại pact tests, chúng ta có kết quả “fail” màu đỏ:

Upload image

Ngoài ra, chúng ta còn có thể xem được version nào của Consumer và Provider tương thích với nhau để có thể chọn ra bản deploy an toàn:

Upload image

Ở đây chúng ta thấy là Consumer version 1.0.0 tương thích với Provider version 1.0.0 và không tương thích với Provider version 1.1.0.

Chúng ta còn có thể kiểm tra version có thích hợp để deploy hay không thông qua Pact Broker Client CLI. Chi tiết cài đặt và sử dụng, bạn đọc có thể xem tại link này.

Sau khi cài đặt CLI, chúng ta dùng tính năng “Can I deploy” để kiểm tra Provider version 1.1.0, chạy câu lệnh sau ở terminal: pact-broker can-i-deploy --broker-base-url http://localhost:9292 --pacticipant example-provider --version 1.1.0

Kết quả được hiển thị như bên dưới:

Upload image

Như vậy, chúng ta có thể thấy Pact Broker giúp chúng ta dễ dàng chia sẻ file contract cũng như kết quả contract testing. Đồng thời cung cấp khả năng kiểm tra tính tương thích giữa các version của các service với nhau, qua đó giúp chúng ta đảm bảo không deploy một version gây ra lỗi cho những service khác trong hệ thống.

Trong bài viết tiếp theo, chúng ta sẽ tìm hiểu về Pact và Pactflow, cách mà hệ sinh thái này đã đơn giản hóa việc áp dụng Contract Testing với sự ra đời của Bidirectional Contract Testing.

Atekco - Home for Authentic Technical Consultants