Skip to content

A Swift version of Pact. Implements Pact Specification Version 3.

License

Notifications You must be signed in to change notification settings

ittybittyapps/PactSwift

 
 

Repository files navigation

PactSwift

Build codecov MIT License PRs Welcome! slack Twitter

PactSwift logo

This framework provides a Swift DSL for generating and verifying Pact contracts. It provides the mechanism for Consumer-Driven Contract Testing between dependent systems where the integration is based on HTTP. PactSwift allows you to test the communication boundaries between your app and services it integrates with.

PactSwift implements (most of) [Pact Specification v4][pact-specification-v4] and runs the mock service "in-process". No need to set up any external mock services, stubs or extra tools 🎉. It supports contract creation along with client verification. It also supports provider verification and interaction with a Pact broker.

Installation

Note: see Upgrading for notes on upgrading and breaking changes.

Swift Package Manager

Xcode

  1. Enter https://github.com/surpher/PactSwift in Choose Package Repository search bar
  2. Optionally set a minimum version when Choosing Package Options
  3. Add PactSwift to your test target. Do not embed it in your application target.

Package.swift

dependencies: [
    .package(url: "https://github.com/surpher/PactSwift.git", .upToNextMinor(from: "2.0.0"))
]

Linux

Linux Installation Instructions

When using PactSwift on a Linux platform you will need to compile your own libpact_ffi.so library for your Linux distribution from pact-reference/rust/pact_ffi or fetch a Pact FFI Library x.y.z from pact-reference releases.

It is important that the version of libpact_ffi.so you build or fetch is compatible with the header files provided by PactMockServer. See release notes for details.

See /Scripts/build_libpact_ffi for some inspiration building libraries from Rust code. You can also go into pact-swift-examples and look into the Linux example projects. There is one for consumer tests and one for provider verification. They contain the GitHub Workflows where building a pact_ffi .so binary and running Pact tests is automated with scripts.

When testing your project you can either set LD_LIBRARY_PATH pointing to the folder containing your libpact_ffi.so:

export LD_LIBRARY_PATH="/absolute/path/to/your/rust/target/release/:$LD_LIBRARY_PATH"
swift build
swift test -Xlinker -L/absolute/path/to/your/rust/target/release/

or you can move your libpact_ffi.so into /usr/local/lib:

mv /path/to/target/release/libpact_ffi.so /usr/local/lib/
swift build
swift test -Xlinker -L/usr/local/lib/

NOTE:

Writing Pact tests

  • Instantiate a Pact object by defining pacticipants,
  • Instantiate a PactBuilder object,
  • Define the state of the provider for an interaction (one Pact test),
  • Define the expected request for the interaction,
  • Define the expected response for the interaction,
  • Run the test by making the API request using your API client and assert what you need asserted,
  • When running on CI share the generated Pact contract file with your provider (eg: upload to a Pact Broker),
  • When automating deployments in a CI step run can-i-deploy and if computer says OK, deploy with confidence!

Example Consumer Tests

import XCTest
import PactSwift

@testable import ExampleProject

class PassingTestsExample: XCTestCase {

  var builder: PactBuilder!

  override func setUpWithError() throws {
    try super.setUpWithError()

    guard builder == nil else {
      return
    }

    let pact = try Pact(consumer: "Consumer", provider: "Provider")
      .withSpecification(.v4)

    let config = PactBuilder.Config(pactDirectory: ProcessInfo.processInfo.environment["PACT_OUTPUT_DIR"])
    builder = PactBuilder(pact: pact, config: config)
  }

  // MARK: - Tests

  func testGetUsers() {
    try builder.
      .uponReceiving("A request for a list of users")
      .given(ProviderState(description: "users exist", params: ["first_name": "John", "last_name": "Tester"])
      .withRequest(
        method: .GET,
        path: "/api/users",
      )      
      .willRespond(with: 200) { response in 
        try response.jsonBody(
          .like(
            [
              "page": .like(1),
              "per_page": .like(20),
              "total": .randomInteger(20...500),
              "total_pages": .like(3),
              "data": .eachLike( 
                [
                  "id": .randomUUID(like: UUID()),
                  "first_name": .like("John"),
                  "last_name": .like("Tester"),
                  "renumeration": .decimal(125_000.00)
                ]
              )
            ]
          )
        )
      }
      
      try await builder.verify { ctx in 
        let apiClient = RestManager(baseUrl: ctx.mockServerURL)
        let users = try await apiClient.getUsers()
                
        XCTAssertEqual(users.first?.firstName, "John")
        XCTAssertEqual(users.first?.lastName, "Tester")
        XCTAssertEqual(users.first?.renumeration, 125_000.00)
      }
    }
  }

  // Another Pact test example...
  func testCreateUser() {
    try builder.
      .uponReceiving("A request to create a user")
      .given(ProviderState(description: "user does not exist", params: ["first_name": "John", "last_name": "Appleseed"])
      .withRequest(.POST, regex: #"^/\w+/group/([a-z])+/users$"#, example:"/api/group/whoopeedeedoodah/users") { request in
        try request.jsonBody(
          .like(
            [
              // You can use matchers and generators here too, but are an anti-pattern.
              // You should be able to have full control of your requests.
              "first_name": "John",
              "last_name": "Appleseed"
            ]
          )
        )
      }
      .willRespond(with: 201) { response in 
        try response.jsonBody(
          .like(
            [
              "identifier": .randomUUID(like: UUID()),
              "first_name": .like("John"),
              "last_name": .like("Appleseed")
            ]
          )
        )
      }
      
      try await builder.verify { ctx in 
        let apiClient = RestManager(baseUrl: ctx.mockServerURL)
        let user = try await apiClient.createUser(firstName: "John", lastName: "Appleseed")
                
        XCTAssertEqual(user.firstName, "John")
        XCTAssertEqual(user.lastName, "Appleseed")
        XCTAssertFalse(user.identifier.isEmpty)
      }
   
  }
}

The PactBuilder holds all the interactions between your consumer and a provider. As long as the consumer and provider names remain consistent between tests they will be accumulated into the same output pact .json.

Suggestions to improve this are welcome! See contributing.

References:

Generated Pact contracts

Generated Pact contracts are written to the directory configured in the PactBuilder.Config.

    let pact = try Pact(consumer: "Consumer", provider: "Provider")
      .withSpecification(.v4)

    let config = PactBuilder.Config(pactDirectory: ProcessInfo.processInfo.environment["PACT_OUTPUT_DIR"])
    builder = PactBuilder(pact: pact, config: config)

Sharing Pact contracts

If your setup is correct and your tests successfully finish, you should see the generated Pact files in your nominated folder as _consumer_name_-_provider_name_.json.

When running on CI use the pact-broker command line tool to publish your generated Pact file(s) to your Pact Broker or a hosted Pact broker service. That way your API-provider team can always retrieve them from one location, set up web-hooks to trigger provider verification tasks when pacts change. Normally you do this regularly in you CI step/s.

See how you can use a simple Pact Broker Client in your terminal (CI/CD) to upload and tag your Pact files. And most importantly check if you can safely deploy a new version of your app.

Provider verification

In your unit tests suite, prepare a Pact Provider Verification unit test:

  1. Start your local Provider service
  2. Optionally, instrument your API with ability to configure provider states
  3. Run the Provider side verification step

To dynamically retrieve pacts from a Pact Broker for a provider with token authentication, instantiate a PactBroker object with your configuration:

// The provider being verified
let provider = ProviderVerifier.Provider(port: 8080)

// The Pact broker configuration
let pactBroker = PactBroker(
  url: URL(string: "https://broker.url/")!,
  auth: auth: .token(PactBroker.APIToken("auth-token")),
  providerName: "Your API Service Name"
)

// Verification options
let options = ProviderVerifier.Options(
  provider: provider,
  pactsSource: .broker(pactBroker)
)

// Run the provider verification task
ProviderVerifier().verify(options: options) {
  // do something (eg: shutdown the provider)
}

To validate Pacts from local folders or specific Pact files use the desired case.

Examples
// All Pact files from a directory
ProviderVerifier()
  .verify(options: ProviderVerifier.Options(
    provider: provider,
    pactsSource: .directories(["/absolute/path/to/directory/containing/pact/files/"])
  ),
  completionBlock: {
    // do something
  }
)
// Only the specific Pact files
pactsSource: .files(["/absolute/path/to/file/consumerName-providerName.json"])
// Only the specific Pact files at URL
pactsSource: .urls([URL(string: "https://some.base.url/location/of/pact/consumerName-providerName.json")])

Submitting verification results

To submit the verification results, provide PactBroker.VerificationResults object to pactBroker.

Example

Set the provider version and optional provider version tags. See version numbers for best practices on Pact versioning.

let pactBroker = PactBroker(
  url: URL(string: "https://broker.url/")!,
  auth: .token("auth-token"),
  providerName: "Some API Service",
  publishResults: PactBroker.VerificationResults(
    providerVersion: "v1.0.0+\(ProcessInfo.processInfo.environment["GITHUB_SHA"])",
    providerTags: ["\(ProcessInfo.processInfo.environment["GITHUB_REF"])"]
  )
)

For a full working example of Provider Verification see Pact-Linux-Provider project in pact-swift-examples repository.

Matching

In addition to verbatim value matching, you can use a set of useful matching objects that can increase expressiveness and reduce brittle test cases.

See Wiki page about Matchers for a list of matchers PactSwift implements and their basic usage.

Or peek into /Sources/Matchers/.

Example Generators

In addition to matching, you can use a set of example generators that generate random values each time you run your tests.

In some cases, dates and times may need to be relative to the current date and time, and some things like tokens may have a very short life span.

Example generators help you generate random values and define the rules around them.

See Wiki page about Example Generators for a list of example generators PactSwift implements and their basic usage.

Or peek into /Sources/ExampleGenerators/.

Demo projects

PactSwift - Consumer PactSwift - Provider

See pact-swift-examples for more examples of how to use PactSwift.

Contributing

See:

Acknowledgements

This project takes inspiration from pact-consumer-swift and pull request Feature/native wrapper PR.

Logo and branding images provided by @cjmlgrto.

About

A Swift version of Pact. Implements Pact Specification Version 3.

Resources

License

Code of conduct

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 89.4%
  • Shell 10.6%