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