defmodule ShouldI do @moduledoc """ ShouldI is a testing DSL around ExUnit. ShouldI supports with blocks for nested contexts, convenience apis for behavioral naming. ## Examples ~~~ defmodule MyFatTest do with "necessary_key" do setup context do assign context, necessary_key: :neccessary_value end should( "have necessary key", context ) do assert context.necessary_key == :necessary_value end with "sometimes_necessary_key" do setup context do assign context, :sometimes_necessary_key, :sometimes_neccessary_value end # matchers to handle common testing idioms should_match_key sometimes_necessary_key: :sometimes_necessary_value end end ~~~ ShouldI provides support for common idioms through matchers. For example, these matchers are for plug: ~~~ should_respond_with :success should_match_body_to "
~~~ """ defmacro __using__(_) do quote do use ExUnit.Case, except: [setup: 1, setup: 2] import ExUnit.Callbacks, except: [setup: 1, setup: 2] import ShouldI, except: [setup: 1, setup: 2] import ShouldI.OuterSetup end end @doc """ Sometimes, when running a test concurrently, it's helpful to generate a unique identifier so resources don't collide. This macro creates ids, optionally appended ## examples assert {:ok, _} = join_chatroom(uid("discussion"), Mock.user) """ defmacro uid(id \\ nil) do {function, _} = __CALLER__.function if String.starts_with?(Atom.to_string(function), "test ") do "#{__CALLER__.module}.#{function} #{id}" else quote do "#{var!(context).case}.#{var!(context).test} #{unquote(id)}" end end end @doc """ Create a test case. This macro thinly wraps ExUnit's test macro and names the test case with "should". ## examples should "add two numbers", do: (assert 2 + 2 = 4) """ defmacro should(name, options) do quote do test("should #{unquote name}", unquote(options)) end end @doc """ Create a test case with context. This macro thinly wraps ExUnit's test macro and names the test case with "should". ## examples should "check conext for :key", do: (assert context.key == :value) """ defmacro should(name, context, options) do quote do test("should #{unquote name}", unquote(context), unquote(options)) end end @doc """ Wrap ExUnit's setup macro, but allow nesting. Setups will be chained with the outer most modules running first. """ defmacro setup(context_name, [do: block]) do quote do ExUnit.Callbacks.setup(context) do {:ok, unquote(context_name)} = @calling_module.__ex_unit__(:setup, context) { :ok, unquote(block) } end end end @doc """ Wrap ExUnit's setup macro, without context. """ defmacro setup([do: _block]) do quote do raise "Calling setup without a context is unsupported" end end @doc """ A function for wrapping together common setup code. with is useful for nesting setup requirements: ## example with "a logged in user" do setup do ... setup a logged in user end with "a get to :index" do setup do assign context, response: get(:index) end should_respond_with :success should_match_body_to "some_string_to_match" end end """ defmacro with(description, [do: block]) do this_module = "with #{description}" |> String.split(~r"\W") |> Enum.map(&String.capitalize/1) |> Enum.join calling_module = __CALLER__.module module = Module.concat(calling_module, this_module) quote do defmodule unquote(module) do use ExUnit.Case import ExUnit.Callbacks, except: [setup: 1, setup: 2] import ShouldI import ShouldI.OuterSetup, except: [setup: 1, setup: 2] @calling_module unquote( calling_module ) unquote(block) end end end defmodule OuterSetup do defmacro setup(context_name, [do: block]) do quote do ExUnit.Callbacks.setup(context) do unquote(context_name) = context {:ok, unquote(block)} end end end end @doc """ Shorthand function for assigning context key/value pairs. """ def assign context, options do Dict.merge context, options end end