-
-
Save andrewhao/37c592dd1319cd005d9d29bdf5f9eaa8 to your computer and use it in GitHub Desktop.
Mocking interfaces in Elixir with Mox
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi!
Thank you for writing this post!
Just in case it helps, I noticed that you wrote
module
, like in Ruby, instead ofdefmodule
. Also here you declare the moduleMyApp.AlexaDeviceApi.Http
and after you usedMyApp.AlexaDeviceApi.HttpClient
, I supposed you meant the former.Again, thank you very much for the post! Keep it up!