Skip to content

nonec/PactSwift

 
 

Repository files navigation

PactSwift Release: pre-BETA MIT License

PRs Welcome! Carthage compatible slack Twitter

codecov Test - Xcode (default) Test - Xcode (11.5-beta)

⚠️ NOTE ⚠️
pact-swift is under heavy development and not all features are complete. Not everything is documented properly.

This framework provides a Swift DSL for generating Pact contracts.

Implements Pact Specification v3.

The one major advantage of this framework over pact-consumer-swift is that it does not depend on Ruby Mock Service to be running on your machine (or on CI/CD agent). Also, it does not require you to fiddle with test pre-actions and post-actions.

Installation

Carthage

github "surpher/PactSwift" ~> 0.1
carthage update --platform ios --no-use-binaries

Swift Package Manager (beta)

Add PactSwift as a dependency to your test target in Package.swift:

...
dependencies: [
	.package(url: "https://github.com/surpher/PactSwift.git", .branch("master"))
],
...

Run tests in terminal by providing path to static lib as a linker flag:

swift test -Xlinker -LRelativePathTo/libFolder

⚠️ Using PactSwift through SPM requires you to link a libpact_mock_server.a for the appropriate architecture. You can find them in /Resources/ folder.

You can compile a custom lib from pact-reference/rust codebase.

We're actively looking for an alternative approach to using static libs with SPM!

Xcode setup - Carthage

NOTE: This framework is intended to be used in your test target. Do not embed it into your app bundle!

Setup Framework Build Settings

Framework Search Paths

In your test targets build settings, update Framework Search Paths configuration to include $(PROJECT_DIR)/Carthage/Build/iOS (non-recursive):

framework_search_paths

Runpath Search Paths

In your test targets build settings, update Runpath Search Paths configuration to include $(FRAMEWORK_SEARCH_PATHS):

runpath_search_paths

Destination dir (recommended)

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

⚠️ Sandboxed apps are limited in where they can write the Pact contract file. The default location is the Documents folder in the sandbox (eg: ~/Library/Containers/com.example.your-project-name/Data/Documents) and can not be overriden by the environment variable PACT_DIR.

destination_dir

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 PassingTestsExample: XCTestCase {

  var mockService = MockService(consumer: "Example-iOS-app", provider: "users-service")

  // 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": SomethingLike(1), // We will use matchers here, as we normally care about the types and structure, not necessarily the actual value.
          "per_page": SomethingLike(20),
          "total": SomethingLike(58),
          "total_pages": SomethingLike(3),
          "data": EachLike(
            [
              "id": IntegerLike(1),
              "first_name": SomethingLike("John"),
              "last_name": SomethingLike("Tester"),
              "salary": 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 the 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 testGetUsers_Unauthorised() {
    // ... code
  }
  // etc.
}

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

⚠️ Work in progress ⚠️

Verifying your client against the service you are integrating with

If you set the PACT_DIR environment variable, your Xcode setup is correct and your tests successfully run, then you should see the generated Pact files in: $(PACT_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.

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.

Demo projects

See pact-swift-examples repo.

Contributing

See CODE_OF_CONDUCT.md
See CONTRIBUTING.md

Acknowledgements

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

About

A Swift DSL for creating pact contracts implementing Pact Specification Version 3.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

  • Swift 88.7%
  • C 5.7%
  • Shell 4.8%
  • Objective-C 0.8%