Mocks, Adapters and Microservices

Microservices

API Gateway

Number Porting

Telephony

Billing

Number Details

Call Monitoring

Microservices

API Gateway

Number Porting

Telephony

Billing

Number Details

Call Monitoring

Microservices

Number Porting

Number Details

Number Details API Client

Portability Checks

POST /portability_checks

{ 
  "phone_number": "14072966265" 
}
{ 
  "phone_number": "14072966265", 
  "portable": true 
}

Look up number's location (Number details service)

See if we have coverage

Unit Tests

defmodule NumberDetailsApiClientTest do
  test "number deserializes correctly", %{bypass: bypass} do
    Bypass.expect bypass, fn conn ->
      phone_number = "14072966265"
  
      fake_response = Poison.encode!(
        %{tn: phone_number, city: "ORLANDO", state: "FL"}
      )

      Plug.Conn.resp(conn, 200, fake_response)
    end

    result = NumberDetailsApiClient.lookup([phone_number])

    assert result == 
      %NumberDetailsResult{tn: phone_number, city: "ORLANDO", state: "FL"}
  end
end

Integration Test?

defmodule PortabilityCheckControllerTest do
  test "with a portable number", %{conn: conn} do
    phone_number = "14072966265"

    #TODO: figure out how return the number details

    conn = post conn, portability_checks(conn, :create), 
            phone_numbers: [%{phone_number: phone_number}]

    assert json_response(conn, 200) == %{
      phone_numbers: [
        %{
          phone_number: phone_number,
          portable: true
        }
      ]
    }
  end
end

Integration Test?

How do we make our test not rely on the Number Details service?

Ways we could mock

Use Bypass to stub the service response

Use Mock library to make the API client return a fake response

Build a test version of the Number Detail Service

You shouldn’t mock an API (verb), instead you create a mock (noun) that implements a given API.

-- Jose Valim

Ways we could Mock

Use Bypass to stub the service response

Use Mock library to make the API client return a fake response

Build a test version of the Number Detail Service

Two Adapters

defmodule NumberService.HTTPAdapter do

  def lookup(number) do
    number
    |> make_request
    |> handle_response
  end

  # ...
end
defmodule NumberDetails.TestAdapter do

  @orlando_number "14072966265"
  def orlando_number, do: @orlando_number

  def lookup(@orlando_number) do
    %NumberLookupResult{
      phone_number: @orlando_number,
      city: "ORLANDO",
      state: "FL"
    }
  end
end

Set adapters per environment

# config/config.exs

config :porting_app, :number_details,
 adapter: NumberDetails.HTTPAdapter
# config/test.exs

config :porting_app, :number_details,
 adapter: NumberDetails.TestAdapter

Use the Adapter

number_details_client = 
  Application.get_env(:porting_app, :number_details)[:adapter]


number_details_client.lookup("14072966265")

Integration Test

defmodule PortabilityCheckControllerTest do
  alias NumberDetails.TestAdapter, as: TestNumbers

  test "with a portable number", %{conn: conn} do
    # Call with preconfigured test number
    phone_number = TestNumbers.orlando_number

    conn = post conn, portability_checks(conn, :create), 
             %{phone_number: phone_number}

    assert json_response(conn, 200) ==
      %{ phone_number: phone_number, portable: true }
  end
end

Enhancements

Keep Adapters in Sync - Use Structs

defmodule NumberService.HTTPAdapter do

  def lookup(number) do
    number
    |> make_request
    |> handle_response   
  end

  #...

  defp handle_response(body) do
    Poison.decode!(body, 
      as: %NumberDetailsResult{})
  end
end
defmodule NumberDetails.TestAdapter do

  @orlando_number "14072966265"

  def lookup(@orlando_number) do
    %NumberDetailsResult{
      phone_number: @orlando_number,
      city: "ORLANDO",
      state: "FL"
    }
  end
end
defmodule NumberDetailsResult do
  defstruct [:phone_number, :city, :state] 
end

Enhancements

Keep Adapters in Sync - Use Behaviours

defmodule NumberDetails.Adapter do
  @callback lookup(String.t) :: NumberDetailsResult.t
end
defmodule NumberDetails.HttpAdapter do

  @behaviour NumberDetails.Adapter

  def lookup(numbers) do
    #...
  end
end
defmodule NumberDetails.TestAdapter do

  @behaviour NumberDetails.Adapter

  def lookup(numbers) do
    #...
  end
end

Enhancements

Create a proxy module

defmodule NumberDetails do

  @behaviour NumberDetails.Adapter

  def lookup(numbers) do
    adapter.lookup_numbers(numbers)
  end

  def adapter do
    Application.get_env(:porting_app, :number_details)[:adapter]
  end
end


# Then in app code
NumberDetails.lookup("14072966265")

References

Thank You.

Aaron Renner

Telnyx

@bayfieldcoder

https://github.com/aaronrenner

(We're hiring)

Mocks, Adapters and Microservices

By Aaron Renner

Mocks, Adapters and Microservices

Lightning talk from ElixirConf 2016

  • 3,117