Skip to content

Instantly share code, notes, and snippets.

@andrewhao
Last active April 1, 2018 02:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save andrewhao/37c592dd1319cd005d9d29bdf5f9eaa8 to your computer and use it in GitHub Desktop.
Save andrewhao/37c592dd1319cd005d9d29bdf5f9eaa8 to your computer and use it in GitHub Desktop.
Mocking interfaces in Elixir with Mox
module MyApp.Alexa do
def call do
# ...
get_country_and_zip_code(device_id, consent_token)
|> do_some_other_transformation
# ...
end
defp get_country_and_zip_code(device_id, consent_token) do
url =
"https://api.amazonalexa.com/v1/devices/#{device_id}/settings/address/countryAndPostalCode"
headers = [{"Authorization", "Bearer #{consent_token}"}, {"Accept", "application/json"}]
HTTPoison.get!(url, headers)
|> Map.get(:body)
|> Poison.decode!()
end
end
module MyApp.Alexa do
def call do
# ...
MyApp.AlexaDeviceApi.get_country_and_zip_code(device_id, consent_token)
|> do_some_other_transformation
# ...
end
end
module MyApp.AlexaDeviceApi do
def get_country_and_zip_code(device_id, consent_token) do
url =
"https://api.amazonalexa.com/v1/devices/#{device_id}/settings/address/countryAndPostalCode"
headers = [{"Authorization", "Bearer #{consent_token}"}, {"Accept", "application/json"}]
HTTPoison.get!(url, headers)
|> Map.get(:body)
|> Poison.decode!()
end
end
# test/alexa_test.ex
module MyApp.AlexaTest do
test "call/0 calling the amazon device service" do
# Uh oh, how do I intercept the call to AlexaDeviceApi.get_country_and_zip_code/2?
end
end
module MyApp.Alexa do
# Note how this becomes a module method, and the configuration is pulled from the app config
@alexa_device_api Application.get_env(:my_app, :alexa_device_api)
def call do
@alexa_device_api.get_country_and_zip_code(device_id, consent_token)
|> do_some_other_transformation
# ...
end
end
# AlexaDeviceApi becomes a behaviour that describes an interface to conform to
module MyApp.AlexaDeviceApi do
@callback get_country_and_zip_code(device_id :: String.t(), consent_token :: String.t()) ::
{:ok, map()}
end
# The implementation moves to AlexaDeviceApi.Http, the live implementation
# of the API wrapper.
module MyApp.AlexaDeviceApi.Http do
@behaviour MyApp.AlexaDeviceApi
def get_country_and_zip_code(device_id, consent_token) do
url =
"https://api.amazonalexa.com/v1/devices/#{device_id}/settings/address/countryAndPostalCode"
headers = [{"Authorization", "Bearer #{consent_token}"}, {"Accept", "application/json"}]
HTTPoison.get!(url, headers)
|> Map.get(:body)
|> Poison.decode!()
end
end
# We configure the application to deliver the live, HTTP-based implementation by default
# config/config.exs
config :myapp, :alexa_device_api, MyApp.AlexaDeviceApi.HttpClient
# We define a MockClient in our test_helper...
# test/test_helper.ex
Mox.defmock(MyApp.AlexaDeviceApi.MockClient, for: MyApp.AlexaDeviceApi)
# ...configure it to be picked up by the app when it runs in test mode
# config/test.exs
config :myapp, :alexa_device_api, MyApp.AlexaDeviceApi.MockClient
# ...and define behaviors for it per test
# test/alexa_test.ex
module MyApp.AlexaTest do
# ...
import Mox
# This makes us check whether our mocks have been properly called at the end
# of each test.
setup :verify_on_exit!
test "call/0 calls the amazon device service, then transforms the values" do
# Here, we configure this MockClient to always return a hardcoded value
MyApp.AlexaDeviceApi.MockClient
|> expect(:get_country_and_zip_code, fn _, _ ->
{:ok, %{country: "USA", postal_code: "94105"}}
end)
assert MyApp.AlexaTest.call() == "some expected value"
end
end
# This is a mock, because it verifies the function call specifically
# with the values of the arguments passed to it.
MyApp.AlexaDeviceApi.MockClient
|> expect(:get_country_and_zip_code, fn "abcdef", "12345" ->
{:ok, %{country: "USA", postal_code: "94105"}}
end)
# ...
verify!(MyApp.AlexaDeviceApi.MockClient)
# or: verify_on_exit!
# This, too, is a mock, because it verifies the function call, ignoring the args passed to it
MyApp.AlexaDeviceApi.MockClient
|> expect(:get_country_and_zip_code, fn _, _ ->
{:ok, %{country: "USA", postal_code: "94105"}}
end)
# ...
verify!(MyApp.AlexaDeviceApi.MockClient)
# or: verify_on_exit!
# This is a fake, or a stub. It discards all inputs and returns
# pre-canned output. It does not care whether the function was actually
# called, just that if it ever was, it should return.
MyApp.AlexaDeviceApi.MockClient
|> stub(:get_country_and_zip_code, fn _, _ ->
{:ok, %{country: "USA", postal_code: "94105"}}
end)
@emibloque
Copy link

Hi!

Thank you for writing this post!

Just in case it helps, I noticed that you wrote module, like in Ruby, instead of defmodule. Also here you declare the module MyApp.AlexaDeviceApi.Http and after you used MyApp.AlexaDeviceApi.HttpClient, I supposed you meant the former.

Again, thank you very much for the post! Keep it up!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment