Writing a failing test first encourages the programmer to articulate clearly what they intend to build before building it. This extra awareness gives the programmer more opportunities to notice going in the wrong direction.
Source: Sooner, Not Faster Revisited
Start the application from the features test
ElixirOutsideinTdd.Application.start(nil, [])
Create a simple router with a match all 404
defmodule GreetingWeb do
use Plug.Router
plug :match
plug :dispatch
match _ do
send_resp(conn, 404, "the endpoint not exist")
end
end
Create a simple router
defmodule GreetingWeb do
use Plug.Router
plug :match
plug :dispatch
get "/" do
send_resp(conn, 200, "...")
end
end
Start Cowboy
Plug.Cowboy.child_spec(scheme: :http, plug: GreetingWeb, options: [port: 4000])
Make an HTTP call with HTTPoison
HTTPoison.get!("https://api.github.com")
%HTTPoison.Response{status_code: 200,
headers: [{"content-type", "application/json"}],
body: "{...}"}
Fetch query params in Plug
Plug.Conn.fetch_query_params(conn).query_params
Application
ElixirOutsideinTdd.Application.start(nil, messages_service: FakeMessagesService)
Cowboy
Plug.Cowboy.child_spec(scheme: :http, plug: {GreetingServiceRouter, messages_service: messages_service}, options: [port: 4000])
Router
plug :dispatch, builder_opts()
get "/hello" do
send_resp(conn, 200, opts[:messages_service])
end
defmodule GreetingWebTest do
use ExUnit.Case, async: true
use Plug.Test
@opts GreetingWeb.init([])
test "returns hello world" do
conn = conn(:get, "/hello")
conn = MyRouter.call(conn, @opts)
assert conn.status == 200
assert conn.resp_body == "world"
end
end
Test:
@opts GreetingServiceRouter.init(my_collaborator: ACollaborator)
test "the GreetingService is called with no user" do
conn = conn(:get, "/hello")
conn = MyPlugRouter.call(conn, @opts)
assert conn.resp_body == "Any message"
end
Code:
defmodule MyPlugRouter do
use Plug.Router
plug(:match)
plug(:dispatch, builder_opts())
get "/hello" do
message = opts[:my_collaborator].some_function()
send_resp(conn, 200, message)
end
end
And do not use this thing! 🚫
hour_of_the_day_that_returns = fn(hour) ->
contents =
quote do
defmodule HourOfTheDay do
def hour(), do: unquote(hour)
end
end
Code.eval_quoted(contents
HourOfTheDay
end
def hour_of_the_day_that_returns(hour) do
Code.eval_quoted(
quote do
defmodule HourOfTheDay do
def hour(), do: unquote(hour)
end
end
)
HourOfTheDay
end
def start(type, hour_of_the_day_service: hour_of_the_day_service) do
start(type, Keyword.merge(@opts, hour_of_the_day_service: hour_of_the_day_service))
end
setup do
{:ok, _} = ElixirOutsideinTdd.Application.start(nil,
hour_of_the_day_service: HourOfTheDayServiceThatReturns7
)
on_exit(fn ->
Application.stop(:elixir_outsidein_tdd)
Process.sleep(100)
end)
:ok
end
Replace the Fake HourOfTheDayService with a Mock
defmodule HourOfTheDayService do
@callback hour() :: number
end
Mock (test/support/mocks.ex
)
Mox.defmock(HourOfTheDayServiceMock, for: HourOfTheDayService)
How to use it then:
import Mox
expect(HourOfTheDayServiceMock, :hour, fn -> 8 end)
verify!(HourOfTheDayServiceMock)
Real implementation would be:
defmodule RealHourOfTheDayService do
@behaviour HourOfTheDayService
@impl true
def hour() do
{:ok, %{hour: hour}} = DateTime.now("Etc/UTC")
hour
end
end