Packages

Pavlov is a BDD library for your Elixir projects, allowing you to write expressive unit tests that tell the story of how your application behaves. The syntax tries to follow RSpec's wherever possible.

Current section

Files

Jump to
pavlov lib case.ex
Raw

lib/case.ex

defmodule Pavlov.Case do
@moduledoc """
Use this module to prepare other modules for testing.
## Example
defmodule MySpec do
use Pavlov.Case
it "always passes" do
assert true
end
end
"""
@doc false
defmacro __using__(opts \\ []) do
async = Keyword.get(opts, :async, false)
quote do
use ExUnit.Case, async: unquote(async)
use Pavlov.Callbacks
use Pavlov.Mocks
@stack []
@pending false
Agent.start(fn -> %{} end, name: :pavlov_let_defs)
import Pavlov.Case
import Pavlov.Syntax.Sugar
end
end
@doc """
The cornerstone BDD macro, "it" allows your test to be defined
via a string.
## Example
it "is the truth" do
assert true == true
end
"""
defmacro it(desc, var \\ quote(do: _), contents) do
quote do
defit Enum.join(@stack, "") <> unquote(desc), unquote(var), @pending do
unquote(contents)
end
end
end
@doc """
Allows you specify a pending test, meaning that it is never run.
## Example
xit "is the truth" do
# This will never run
assert true == true
end
"""
defmacro xit(description, var \\ quote(do: _), contents) do
quote do
defit Enum.join(@stack, "") <> unquote(description), unquote(var), true do
unquote(contents)
end
end
end
@doc """
You can nest your tests under a descriptive name.
Tests can be infinitely nested.
"""
defmacro describe(desc, _ \\ quote(do: _), pending \\ false, contents) do
quote do
@stack Enum.concat(@stack, [unquote(desc) <> ", "])
# Closure the old stack so we can use it in defmodule
old_stack = Enum.concat @stack, []
pending = @pending || unquote(pending)
# Defines a new module per describe, thus scoping .let
defmodule Module.concat(__MODULE__, unquote(desc)) do
use ExUnit.Case
@stack old_stack
@pending pending
# Redefine enclosing let definitions in this module
Agent.get(:pavlov_callback_defs, fn dict ->
Stream.filter dict, fn {module, _name} ->
String.starts_with?("#{__MODULE__}", "#{module}") && "#{__MODULE__}" != "#{module}"
end
end)
|> Stream.map(fn {_module, {periodicity, context, fun}} ->
quote do
use Pavlov.Mocks
before(unquote(periodicity), unquote(context), do: unquote(fun))
end
end)
|> Enum.each(&Module.eval_quoted(__MODULE__, &1))
unquote(contents)
# Redefine enclosing let definitions in this module
Agent.get(:pavlov_let_defs, fn dict ->
Stream.filter dict, fn {module, _name} ->
String.starts_with? "#{__MODULE__}", "#{module}"
end
end)
|> Stream.map(fn {_module, {name, fun}} ->
quote do: let(unquote(name), do: unquote(fun))
end)
|> Enum.each(&Module.eval_quoted(__MODULE__, &1))
end
# Cleans context stack
if Enum.count(@stack) > 0 do
@stack Enum.take(@stack, Enum.count(@stack) - 1)
end
end
end
@doc """
Defines a group of tests as pending.
Any other contexts nested within an xdescribe will not run
as well.
"""
defmacro xdescribe(desc, _ \\ quote(do: _), contents) do
quote do
describe unquote(desc), _, true, unquote(contents)
end
end
@doc """
Allows lazy initialization of subjects for your tests.
Subjects created via "let" will never leak into other
contexts (defined via "describe" or "context"), not even
those who are children of the context where the lazy subject
is defined.
Example:
let :lazy do
"oh so lazy"
end
it "lazy initializes" do
assert lazy == "oh so lazy"
end
"""
defmacro let(name, contents) do
quote do
require Pavlov.Utils.Memoize, as: Memoize
Memoize.defmem unquote(name)(), do: unquote(contents[:do])
Agent.update(:pavlov_let_defs, fn(map) ->
Dict.put_new map, __MODULE__, {unquote(Macro.escape name), unquote(Macro.escape contents[:do])}
end)
end
end
@doc false
defmacro defit(message, var \\ quote(do: _), pending \\ false, contents) do
contents =
case contents do
[do: _] ->
quote do
unquote(contents)
:ok
end
_ ->
quote do
try(unquote(contents))
:ok
end
end
var = Macro.escape(var)
contents = Macro.escape(contents, unquote: true)
quote bind_quoted: binding do
message = :"#{message}"
Pavlov.Case.__on_definition__(__ENV__, message, pending)
def unquote(message)(unquote(var)) do
Pavlov.Utils.Memoize.flush
unquote(contents)
end
end
end
@doc false
def __on_definition__(env, name, pending \\ false) do
mod = env.module
tags = Module.get_attribute(mod, :tag) ++ Module.get_attribute(mod, :moduletag)
if pending do tags = [tags|[:pending]] end
tags = tags |> normalize_tags |> Map.merge(%{line: env.line, file: env.file})
Module.put_attribute(mod, :ex_unit_tests,
%ExUnit.Test{name: name, case: mod, tags: tags})
Module.delete_attribute(mod, :tag)
end
defp normalize_tags(tags) do
Enum.reduce Enum.reverse(tags), %{}, fn
tag, acc when is_atom(tag) -> Map.put(acc, tag, true)
tag, acc when is_list(tag) -> Dict.merge(acc, tag)
end
end
end