/pact_elixir

Elixir version of Pact. Enables consumer driven contract testing, providing a mock service and DSL for the consumer project, and interaction playback and verification for the service provider project.

Primary LanguageElixirMIT LicenseMIT

PactElixir

This library is an Elixir wrapper for the pact-reference implementation

And it is in PRE alpha shape without any semantic versioning or documentation.

Build Status Coverage Status SourceLevel Inline docs

It is not yet usable as many needed parts like publishing a pact file to a broker or pact verification on the provider side are still missing. Also there is no documentation available, yet.

Development

Discussion about the development of this library takes places in the #pact-elixir Channel at https://pact-foundation.slack.com/.

Installation

You need Rust in order to build and install the package.

If available in Hex, the package can be installed by adding pact_elixir to your list of dependencies in mix.exs:

def deps do
  [
    {:pact_elixir, "~> 0.5.2"}
  ]
end

Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/pact_elixir.

Examples

This is an example test case:

defmodule PactElixir.PactMockServerTest do
  use ExUnit.Case
  alias PactElixir.{PactMockServer, ServiceProvider}
  import PactElixir.Dsl

  setup do
    options = %{provider: "SomeProvider", consumer: "SomeConsumer"}
    provider = new_service_provider(options)

    {:ok, mock_server_pid} = start_supervised({PactMockServer, provider})
    {:ok, mock_server_pid: mock_server_pid, provider: provider}
  end

  describe "SomeProvider talks to SomeConsumer" do
    test "some basic test", %{mock_server_pid: mock_server_pid} do
      expected = "{groups: ['Editors'], id: 123, username: 'UserA'}"

      user(mock_server_pid)

      assert expected == user(mock_server_pid).body
      assert {:ok} == PactMockServer.write_pact_file(mock_server_pid)
    end
  end

  defp get_request(path, mock_server_pid) when is_pid(mock_server_pid) do
    get_request(path, PactMockServer.port(mock_server_pid))
  end

  defp get_request(path, port) when is_number(port) do
    %HTTPoison.Response{} = HTTPoison.get!("http://localhost:#{port}#{path}")
  end

  def user(mock_server_pid) do
    get_request("/users/UserA", mock_server_pid)
  end

  defp new_service_provider(options \\ %{}) do
    options
    |> PactElixir.Dsl.service_provider()
    |> add_interaction(
      "give me foo",
      given("UserA exists and is not an administrator"),
      with_request(method: :get, path: "/users/UserA"),
      will_respond_with(status: 200, body: "{groups: ['Editors'], id: 123, username: 'UserA'}")
    )
  end
end

You should be able to run it with mix test <path_to_test_case>.

Publishing If the test passes, a json file will be created and saved in a new directory - ./pacts.

Currently, pact publishing is not yet implemented in this library. You can run the following bash script for your basic publishing needs:

#!/bin/bash
# This script:
# 1) extracts the name of provider and consumer from the name of a JSON pact file
# e.g.: Consumer1-Provider.json gives you consumer Consumer1 and provider Provider
# 2) reads the JSON pact file from /pacts directory (pacts are stored there by specification)
# 3) publishes pact to Pact Broker

for pact in ./pacts/*.json; do
    pact_name=$(basename $pact)

    consumer=${pact_name%-*}
    provider=${pact_name#*-}
    provider=${provider%.*}

    curl -v -XPUT \-H "Content-Type: application/json" \
    -d@${pact} \
    https://<your-pact-broker-url>/pacts/provider/$provider/consumer/$consumer/version/<some-version-number>
done

Troubleshooting

Compiling NIF crate :pactmockserver (native/pactmockserver)...
could not compile dependency :pact_elixir, "mix compile"
failed. You can recompile this dependency with "mix deps.compile pact_elixir", update it with "mix deps.update pact_elixir" or clean it with "mix deps.clean pact_elixir"

This can be solved by ensuring proper structure of mix.exs.

  1. Ensure you have rustler listed in compilers
  2. Ensure pactmockserver is listed in rustler_crates
def project do
    [
      #...
      compilers: [:phoenix, :rustler] ++ Mix.compilers(),
      rustler_crates: rustler_crates(Mix.env()),
      #...
    ]
  end

(This is an example setting, where you don't want to ship any rustler crates to production. Bottom line is, there has to be a function that returns the crates list, including pactmockserver).

defp rustler_crates(mix_env) when mix_env in [:test, :dev] do
    [
      pactmockserver: [
        path: "deps/pact_elixir/native/pactmockserver",
        mode: (:debug),
      ]
    ]
  end

  defp rustler_crates(_prod) do
    []
  end

Docker

Properly installing Rust should solve a majority of the problems. Just add the following code (this is for a Debian distro, it assumes you have curl installed):

# Rust is required by pact-elixir
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y
ENV PATH=$HOME/.cargo/bin:$PATH