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
endCreate a simple router
defmodule GreetingWeb do
use Plug.Router
plug :match
plug :dispatch
get "/" do
send_resp(conn, 200, "...")
end
endStart 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_paramsApplication
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])
enddefmodule 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
endTest:
@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"
endCode:
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
endAnd 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
enddef hour_of_the_day_that_returns(hour) do
Code.eval_quoted(
quote do
defmodule HourOfTheDay do
def hour(), do: unquote(hour)
end
end
)
HourOfTheDay
enddef 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))
endsetup 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
endReplace the Fake HourOfTheDayService with a Mock
defmodule HourOfTheDayService do
@callback hour() :: number
endMock (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