defmodule Behave do @moduledoc """ ~Ugh baby, `Behave`! Simple BDD style tests for elixir. """ alias Behave.Scenario defmacro __using__(opts) do step_modules = Keyword.get(opts, :steps) if step_modules == nil do raise "Please specify your step implementation modules like so: use Behave, steps: [MyTestSteps, MyOtherTestSteps]." end imports = for module <- step_modules do quote do: import(unquote(module)) end quote do import Behave alias Behave.Scenario unquote_splicing(imports) end end defmacro scenario(title, do: block) do quote do test unquote(title) do var!(scenario) = Behave.Scenario.new() unquote(block) Behave.Scenario.run(var!(scenario)) end end end defmacro given(name, args \\ []) do {module, name, arity} = find_target_fun(__CALLER__.requires, "given " <> name) fun = Function.capture(module, name, arity) quote do var!(scenario) = Behave.__given__(var!(scenario), unquote(fun), unquote(args)) end end defmacro act(name, args \\ []) do {module, name, arity} = find_target_fun(__CALLER__.requires, "act " <> name) fun = Function.capture(module, name, arity) quote do var!(scenario) = Behave.__act__(var!(scenario), unquote(fun), unquote(args)) end end defmacro check(name, args \\ []) do {module, name, arity} = find_target_fun(__CALLER__.requires, "check " <> name) fun = Function.capture(module, name, arity) quote do var!(scenario) = Behave.__check__(var!(scenario), unquote(fun), unquote(args)) end end def mdbg(macro) do IO.puts(Macro.to_string(macro)) macro end def __given__(scenario = %Scenario{}, step, args \\ []) do update_in(scenario.steps, &[{:given, step, args} | &1]) end def __act__(scenario = %Scenario{}, step, args \\ []) do update_in(scenario.steps, &[{:act, step, args} | &1]) end def __check__(scenario = %Scenario{}, step, args \\ []) do update_in(scenario.steps, &[{:check, step, args} | &1]) end defp find_target_fun(modules, name) do atom_name = string_to_function_name(name) function_name_predicate = fn {name, _arity} -> name == atom_name end target_module = modules |> Enum.find(fn module -> module.__info__(:functions) |> Enum.any?(function_name_predicate) end) if target_module == nil do raise "Tried to find a step implementation for \"#{name}\", but none were defined. Did you forget an import?" end {target_function_name, target_function_arity} = target_module.__info__(:functions) |> Enum.find(function_name_predicate) {target_module, target_function_name, target_function_arity} end def string_to_function_name(string) do String.downcase(string) |> String.trim() |> String.replace(" ", "_") |> String.to_atom() end end