Wow, I provoked a huge wave of discussions, some of them are still beyond of my understanding. Thank you all for bringing your experience and knowledge stones to the base.
What confuses me most is that n my simple app, I have a GenServer defies to be started as a child process wen the app starts:
# application.ex
@impl true
def start(_type, _args) do
...
{PaymentServer.Exchange.RatesMonitor, []}
]
end
where the module RatesMonitor
is a GenServer which hits an external API server every ‘X’ minutes.
I tried to define a behaviour module as follows:
defmodule PaymentServer.Exchange.MonitorApi do
@moduledoc """
Handles Exchange server API calls
"""
@callback get_rates(from_currency :: String.t(), to_currency :: String.t()) ::
{:ok, any()} | {:error, any()}
end
Then in a real API module I declare this behavour:
defmodule PaymentServer.Exchange.AlphaVantageApi do
@behaviour PaymentServer.Exchange.MonitorApi
@moduledoc """
This is a Alpha Vantage API module which provides the exchange rates data
"""
@api_url "http://localhost:4001/query"
@impl true
def get_rates(from_currency \\ "USD", to_currency \\ "JPY") do
url = build_search_url(from_currency, to_currency)
case Req.get(url) do
{:error, reason} -> {:error, reason}
{:ok, response} -> response
end
end
...
end
Then in the GenServer module, I call it as follows:
defmodule PaymentServer.Exchange.RatesMonitor do
@moduledoc """
This is the implementation of an exchange rates monitor server
"""
use GenServer
...
defp fetch_rates(%{from_currency: from_currency, to_currency: to_currency} = state) do
api_module =
Application.get_env(:payment_server, :api_module, PaymentServer.Exchange.AlphaVantageApi)
response = api_module.get_rates(from_currency, to_currency)
rate = Parser.parse(response)
%{state | from_currency: from_currency, to_currency: to_currency, rate: rate}
end
end
To be able to use a mock API module in tests, I created the following module:
defmodule PaymentServer.Exchange.MockAlphaVantageApi do
@behaviour PaymentServer.Exchange.MonitorApi
@impl true
def get_rates(from_currency, to_currency) do
"""
status: 200,
body: %{
"Realtime Currency Exchange Rate" => %{
"1. From_Currency Code" => "EUR",
"2. From_Currency Name" => "Euro",
"3. To_Currency Code" => "USD",
"4. To_Currency Name" => "US Dollar",
"5. Exchange Rate" => "3.2045",
"6. Last Refreshed" => "2024-03-31 16:47:21.920729Z",
"7. Time Zone" => "UTC",
"8. Bid Price" => "3.2045",
"9. Ask Price" => "3.2045"
}
},
"""
end
end
and put these lines in the config/tests.exs
file:
config :payment_server,
api_module: PaymentServer.Exchange.MockAlphaVantageApi
A simple tests where I tried to check if the mock module would be used:
defmodule PaymentServer.Exchange.MockAlphaVantageApiTest do
use ExUnit.Case, async: true
test "it should call mocked API" do
response = PaymentServer.Exchange.MonitorApi.get_rates("eur", "usd")
IO.inspect response
end
end
Before running the test, I stopped the API running in a Docker container and run mix test test/payment_server/exchange/mock_alpha_vantage_api_test.exs
which failed with:
** (Mix) Could not start application payment_server: PaymentServer.Application.start(:normal, []) returned an error: shutdown: failed to start child: PaymentServer.Exchange.RatesMonitor
** (EXIT) an exception was raised:
** (UndefinedFunctionError) function PaymentServer.Exchange.MockAlphaVantageApi.get_rates/2 is undefined (module PaymentServer.Exchange.MockAlphaVantageApi is not available)
PaymentServer.Exchange.MockAlphaVantageApi.get_rates(:usd, :eur)
(payment_server 0.1.0) lib/payment_server/exchange/rates_monitor.ex:64: PaymentServer.Exchange.RatesMonitor.fetch_rates/1
(payment_server 0.1.0) lib/payment_server/exchange/rates_monitor.ex:36: PaymentServer.Exchange.RatesMonitor.init/1
(stdlib 4.3.1.2) gen_server.erl:851: :gen_server.init_it/2
(stdlib 4.3.1.2) gen_server.erl:814: :gen_server.init_it/6
(stdlib 4.3.1.2) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
So the question is why, despite all the above settings, the app could not start the GenServer module, PaymentServer.Exchange.RatesMonitor
?
It bothers me why to run tests, I still need to have API instance running in a Docker container? Even when using the above Mimic lib, (which mock fine the requests), it is still needed.