Skip to content

Instantly share code, notes, and snippets.

@joebew42
Last active February 15, 2020 20:37
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 joebew42/a5b29073f2f3e7d639f919d391dde8e8 to your computer and use it in GitHub Desktop.
Save joebew42/a5b29073f2f3e7d639f919d391dde8e8 to your computer and use it in GitHub Desktop.
Some of the material for the tdd outside in hands-on session in Elixir

Materials for the TDD Outside-in hands-on session in Elixir

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

Where is my business?

Code Snippets

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

How to dependency injection

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

Testing Plug

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

Dependency Injection with Plug

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

Do not use a Mock 😄

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

Merge Keyword List

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

Start and stop the server

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

BONUS

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment