Swiftpack.co -  surpher/PactSwift as Swift Package
Swiftpack.co is a collection of thousands of indexed Swift packages. Search packages.
surpher/PactSwift
A Swift DSL for creating pact contracts implementing Pact Specification Version 3.
.package(url: "https://github.com/surpher/PactSwift.git", from: "v0.4.2")

PactSwift

Release: pre-BETA MIT License PRs Welcome! slack Twitter codecov Build

PactSwift logo

This framework provides a Swift DSL for generating Pact contracts.

It implements Pact Specification v3 and takes advantage of libpact_mock_server_ffi by running it "in process". No need to set up any specific mock services or extra tools 🎉.

Installation

Note: see Upgrading for notes on upgrading from 0.3 to 0.4

Swift Package Manager

Xcode

  1. Enter https://github.com/surpher/PactSwift in Choose Package Repository search bar
  2. Use minimum version 0.4.2 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", .upToNextMajor(from: "0.4.2"))
]

Carthage

# Cartfile
github "surpher/PactSwift" ~> 0.4
carthage update --use-xcframeworks

NOTE:

Generated Pact contracts

By default, generated Pact contracts are written to /tmp/pacts. Edit your scheme and add PACT_OUTPUT_DIR environment variable (in Run section) with path to the directory you want your Pact contracts to be written to.

Sandboxed apps (macOS) are limited in where they can write the Pact contract files. The default location is the Documents folder in the sandbox (eg: ~/Library/Containers/com.example.your-project-name/Data/Documents). Setting the environment variable PACT_OUTPUT_DIR might not work without some extra setting. Look at the logs in debug area for the Pact file location.

Writing Pact tests

  • Instantiate a MockService object by defining pacticipants,
  • 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,
  • Share the generated Pact contract file with your provider (eg: upload to a Pact Broker),
  • Run can-i-deploy (eg: on your CI/CD) to deploy with confidence.

Example Test

import XCTest
import PactSwift

@testable import ExampleProject

class MockServiceWrapper {
  static let shared = MockServiceWrapper()
  var mockService: MockService

  private init() {
    mockService = MockService(consumer: "Example-iOS-app", provider: "some-api-service")
  }
}

// MARK: - XCTestCase

class PassingTestsExample: XCTestCase {

  var mockService = MockServiceWrapper.shared.mockService

  // MARK: - Tests

  func testGetUsers() {
    // #1 - Define the API contract by configuring how `mockService`, and consequently the "real" API, will behave for this specific API request we are testing here
    mockService

      // #2 - Define the interaction description and provider state for this specific API request that we are testing
      .uponReceiving("A request for a list of users")
      .given(ProviderState(description: "users exist", params: ["first_name": "John", "last_name": "Tester"])

      // #3 - Define the request we promise our API consumer will make
      .withRequest(
        method: .GET,
        path: "/api/users",
        headers: nil, // `nil` means we (and the API Provider) should not care about headers. If there are values there, fine, we're just not _demanding_ anything.
        body: nil // same as with headers
      )

      // #4 - Define what we expect `mockService`, and consequently the "real" API, to respond with for this particular API request we are testing
      .willRespondWith(
        status: 200,
        headers: nil, // `nil` means we don't care what the headers returned from the API are. If there are values in the header, fine, we're just not _demanding_ anything in the header.
        body: [
          "page": Matcher.SomethingLike(1), // We will use matchers here, as we normally care about the types and structure, not necessarily the actual value.
          "per_page": Matcher.SomethingLike(20),
          "total": ExampleGenerator.RandomInt(min: 20, max: 500),
          "total_pages": Matcher.SomethingLike(3),
          "data": Matcher.EachLike(
            [
              "id": ExampleGenerator.RandomUUID(), // We can also use example generators with Pact Spec v3
              "first_name": Matcher.SomethingLike("John"),
              "last_name": Matcher.SomethingLike("Tester"),
              "salary": Matcher.DecimalLike(125000.00)
            ]
          )
        ]
      )

    // #5 - Fire up our API client
    let apiClient = RestManager()

    // Run a Pact test and assert **our** API client makes the request exactly as we promised above
    mockService.run(waitFor: 1) { [unowned self] completed in

      // #6 - _Redirect_ your API calls to the address MockService runs on - replace base URL, but path should be the same
      apiClient.baseUrl = self.mockService.baseUrl

      // #7 - Make the API request.
      apiClient.getUsers() { users in

          // #8 - Test that **our** API client handles the response as expected. (eg: `getUsers() -> [User]`)
          XCTAssertEqual(users.count, 20)
          XCTAssertEqual(users.first?.firstName, "John")
          XCTAssertEqual(users.first?.lastName, "Tester")
        }

        // #9 - Notify MockService we're done with our test, else your Pact test will time out.
        completed()
      }
    }
  }

  // More tests for other interactions and/or provider states...
  func testCreateUser() {
    mockService
      .uponReceiving("A request to create a user")
      .given(ProviderState(description: "user does not exist", params: ["first_name": "John", "last_name": "Appleseed"])
      .withRequest(
        method: .POST,
        path: Matcher.RegexLike("/api/group/whoopeedeedoodah/users", term: #"^/\w+/group/([a-z])+/users$"#),
        body: [
          "first_name": "John",
          "last_name": "Appleseed",
          "age": Matcher.SomethingLike(42),
          "dob": Matcher.RegexLike("15-07-2001", term: #"\d{2}-\d{2}-\d{4}"#),
          "trivia": [
            "favourite_words": Matcher.EachLike("foo"),
            "bar": Matcher.IncludesLike("baz")
          ]
        ]
      )
      .willRespondWith(
        status: 201
      )

   let apiClient = RestManager()

    mockService.run { completed in
     // trigger your network request and assert the expectations
     completed()
    }
  }
  // etc.
}

MockService holds all the interactions between your consumer and a provider. For each test method, a new instance of XCTestCase class is allocated and its instance setup is executed. That means each test has it's own instance of var mockService = MockService(). Hence the reason we're using a singleton here to keep a reference to one instance of MockService for all the Pact tests.
I'm open for alternative, better ideas!

References:

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 verbatim value matching and some helpful matchers, 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/.

Verifying your client against the service you are integrating with

If you set the PACT_OUTPUT_DIR environment variable, your Xcode setup is correct and your tests successfully run, then you should see the generated Pact files in: $(PACT_OUTPUT_DIR)/_consumer_name_-_provider_name_.json.

Publish your generated Pact file(s) to your Pact Broker or a hosted service, so that your API-provider team can always retrieve them from one location, even when pacts change. Normally you do this regularly in you CI step/s.

See how you can use 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.

Objective-C support

PactSwift can be used in your Objective-C project with a couple of limitations, e.g. initializers with multiple optional arguments are limited to only one or two available initializers. See Demo projects repository for more examples.

Demo projects

PactSwift demo projects

More in pact-swift-examples repo.

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.

GitHub

link
Stars: 14
Last commit: 1 hour ago

Ad: Job Offers

iOS Software Engineer @ Perry Street Software
Perry Street Software is Jack’d and SCRUFF. We are two of the world’s largest gay, bi, trans and queer social dating apps on iOS and Android. Our brands reach more than 20 million members worldwide so members can connect, meet and express themselves on a platform that prioritizes privacy and security. We invest heavily into SwiftUI and using Swift Packages to modularize the codebase.

Release Notes

v0.4.2 - Bugfix
2 days ago

Fixed

  • #68 - Fixes a bug where EachLike matcher rules were not written to Pact contract

Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API