Skip to content

Commit 8f57b4c

Browse files
author
José Valim
committed
Add metadata support to ExUnit
Defining setup/teardown/test explicitly is now deprecated. They should now be defined via their respective macros: def setup do ... end Now becomes: setup do ... end When defined using the new format, setup callbacks can return metadata: setup do [do_this: :do_that] end And now the test macro can access this metadata: test "check metadata", data do assert data[:do_this] == :do_that end
1 parent 19d402b commit 8f57b4c

File tree

10 files changed

+262
-104
lines changed

10 files changed

+262
-104
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
* enhancements
2+
* [ExUnit] Tests can now receive metadata set on setup/teardown callbacks
23
* [IO] Add `IO.ANSI` to make it easy to write ANSI escape codes
34
* [Kernel] Better support for Unicode lists
45
* [Kernel] Reduce variables footprint in `case`/`receive` clauses
@@ -16,6 +17,7 @@
1617
* [Kernel] `before_compile` and `after_compile` callbacks now receive the environment as first argument instead of the module
1718

1819
* deprecations
20+
* [ExUnit] Explicitly defined test/setup/teardown functions are deprecated
1921
* [Kernel] Tidy up and clean `quote` API
2022
* [Kernel] Old `:local.(args)` syntax is deprecated
2123
* [Process] `Process.self` is deprecated in favor `Kernel.self`

lib/elixir/test/elixir/file_test.exs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@ defmodule FileTest do
1010

1111
# cp_r/c is managed in setup/teardown because if it is stored in
1212
# the repository, reltool can't build a release
13-
def setup do
13+
setup do
1414
src = fixture_path("cp_r")
1515
:file.make_symlink 'certainly/invalid', Path.join([src, "c"])
16+
[]
1617
end
1718

18-
def teardown do
19+
teardown do
1920
src = fixture_path("cp_r")
2021
File.rm Path.join([src, "c"])
22+
[]
2123
end
2224

2325
test :cp_with_src_file_and_dest_file do
@@ -677,14 +679,16 @@ defmodule FileTest do
677679

678680
# cp_r/c is managed in setup/teardown because if it is stored in
679681
# the repository, reltool can't build a release
680-
def setup do
682+
setup do
681683
src = fixture_path("cp_r")
682684
:file.make_symlink 'certainly/invalid', Path.join([src, "c"])
685+
[]
683686
end
684687

685-
def teardown do
688+
teardown do
686689
src = fixture_path("cp_r")
687690
File.rm Path.join([src, "c"])
691+
[]
688692
end
689693

690694
test :rm_file do

lib/elixir/test/elixir/kernel_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ defmodule KernelTest do
5858
end
5959

6060
test :function_from___ENV__ do
61-
assert __ENV__.function == { :test_function_from___ENV__, 0 }
61+
assert __ENV__.function == { :test_function_from___ENV__, 1 }
6262
end
6363

6464
defp x(value) when value in [1,2,3], do: true

lib/elixir/test/elixir/macro_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ defmodule MacroTest do
296296

297297
test :env_stacktrace do
298298
env = __ENV__.file("foo").line(12)
299-
assert env.stacktrace == [{ __MODULE__, :test_env_stacktrace, 0, [file: "foo", line: 12] }]
299+
assert env.stacktrace == [{ __MODULE__, :test_env_stacktrace, 1, [file: "foo", line: 12] }]
300300
env = env.function(nil)
301301
assert env.stacktrace == [{ __MODULE__, :__MODULE__, 0, [file: "foo", line: 12] }]
302302
env = env.module(nil)

lib/ex_unit/lib/ex_unit.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@ defmodule ExUnit do
1313
1414
# 2) Next we create a new TestCase and use ExUnit.Case
1515
defmodule AssertionTest do
16-
# 3) Notice we pass async: true, this runs the test case in parallel
16+
# 3) Notice we pass async: true, this runs the test case
17+
# concurrently with other test cases
1718
use ExUnit.Case, async: true
1819
19-
# 4) A test is a method which name finishes with _test
20-
def test_always_pass do
20+
# 4) A test is a function whose name starts with
21+
# test and receives a context
22+
def test_always_pass(_) do
2123
assert true
2224
end
2325

lib/ex_unit/lib/ex_unit/callbacks.ex

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
defmodule ExUnit.Callbacks do
2+
@moduledoc %B"""
3+
This module defines four callbacks: `setup_all`, `teardown_all`,
4+
`setup` and `teardown`. Those callbacks are defined via macros
5+
and receives a keyword list of metadata. The callback may
6+
optionally define extra data which will be available in the test
7+
cases.
8+
9+
## Examples
10+
11+
defmodule AssertionTest do
12+
use ExUnit.Case, async: true
13+
14+
setup do
15+
IO.puts "This is a setup callback"
16+
17+
# Return no extra meta data
18+
[hello: "world"]
19+
end
20+
21+
setup context do
22+
# We can access the test name in the context
23+
IO.puts "Setting up: #{context[:test]}"
24+
25+
# The metadata returned by the previous setup as well
26+
assert context[:hello] == "world"
27+
end
28+
29+
test "always pass" do
30+
assert true
31+
end
32+
end
33+
34+
"""
35+
36+
@doc false
37+
defmacro __using__(_) do
38+
quote do
39+
@exunit_setup []
40+
@exunit_teardown []
41+
@exunit_setup_all []
42+
@exunit_teardown_all []
43+
44+
@before_compile unquote(__MODULE__)
45+
@after_compile unquote(__MODULE__)
46+
import unquote(__MODULE__)
47+
48+
def __exunit__(:setup, context) do
49+
context = __exunit_setup__(context)
50+
ExUnit.Callbacks.__setup__(__MODULE__, context)
51+
end
52+
53+
def __exunit__(:teardown, context) do
54+
context = __exunit_teardown__(context)
55+
ExUnit.Callbacks.__teardown__(__MODULE__, context)
56+
end
57+
58+
def __exunit__(:setup_all, context) do
59+
context = __exunit_setup_all__(context)
60+
ExUnit.Callbacks.__setup_all__(__MODULE__, context)
61+
end
62+
63+
def __exunit__(:teardown_all, context) do
64+
context = __exunit_teardown_all__(context)
65+
ExUnit.Callbacks.__teardown_all__(__MODULE__, context)
66+
end
67+
end
68+
end
69+
70+
@doc false
71+
defmacro __before_compile__(env) do
72+
[ compile_callbacks(env, :exunit_setup),
73+
compile_callbacks(env, :exunit_teardown),
74+
compile_callbacks(env, :exunit_setup_all),
75+
compile_callbacks(env, :exunit_teardown_all) ]
76+
end
77+
78+
defmacro setup(var // quote(do: _), block) do
79+
quote do
80+
name = :"__exunit_setup_#{length(@exunit_setup)}"
81+
defp name, [unquote(Macro.escape var)], [], unquote(Macro.escape block)
82+
@exunit_setup [name|@exunit_setup]
83+
end
84+
end
85+
86+
defmacro teardown(var // quote(do: _), block) do
87+
quote do
88+
name = :"__exunit_teardown_#{length(@exunit_teardown)}"
89+
defp name, [unquote(Macro.escape var)], [], unquote(Macro.escape block)
90+
@exunit_teardown [name|@exunit_teardown]
91+
end
92+
end
93+
94+
defmacro setup_all(var // quote(do: _), block) do
95+
quote do
96+
name = :"__exunit_setup_all_#{length(@exunit_setup_all)}"
97+
defp name, [unquote(Macro.escape var)], [], unquote(Macro.escape block)
98+
@exunit_setup_all [name|@exunit_setup_all]
99+
end
100+
end
101+
102+
defmacro teardown_all(var // quote(do: _), block) do
103+
quote do
104+
name = :"__exunit_teardown_all_#{length(@exunit_teardown_all)}"
105+
defp name, [unquote(Macro.escape var)], [], unquote(Macro.escape block)
106+
@exunit_teardown_all [name|@exunit_teardown_all]
107+
end
108+
end
109+
110+
## Helpers
111+
112+
defp compile_callbacks(env, kind) do
113+
callbacks = Module.get_attribute(env.module, kind) |> Enum.reverse
114+
115+
acc =
116+
Enum.reduce callbacks, quote(do: context), fn(callback, acc) ->
117+
quote do
118+
context = unquote(acc)
119+
Keyword.merge(context, unquote(callback)(context))
120+
end
121+
end
122+
123+
quote do
124+
defp unquote(:"__#{kind}__")(context), do: unquote(acc)
125+
end
126+
end
127+
128+
## Deprecated handling
129+
130+
@doc false
131+
def __after_compile__(env, _binary) do
132+
test_case = env.module
133+
134+
Enum.each [:setup, :teardown, :setup_all, :teardown_all], fn(kind) ->
135+
if Enum.any? 0..2, function_exported?(test_case, kind, &1) do
136+
IO.puts "[Warning] #{kind} callback in #{inspect test_case} is deprecated. Please use the #{kind} macro instead."
137+
end
138+
end
139+
end
140+
141+
@doc false
142+
def __setup__(test_case, context) do
143+
cond do
144+
function_exported?(test_case, :setup, 2) ->
145+
test_case.setup(context, context[:test])
146+
function_exported?(test_case, :setup, 1) ->
147+
test_case.setup(context)
148+
function_exported?(test_case, :setup, 0) ->
149+
test_case.setup
150+
true ->
151+
context
152+
end
153+
end
154+
155+
@doc false
156+
def __teardown__(test_case, context) do
157+
cond do
158+
function_exported?(test_case, :teardown, 2) ->
159+
test_case.teardown(context, context[:test])
160+
function_exported?(test_case, :teardown, 1) ->
161+
test_case.teardown(context)
162+
function_exported?(test_case, :teardown, 0) ->
163+
test_case.teardown
164+
true ->
165+
context
166+
end
167+
end
168+
169+
@doc false
170+
def __teardown_all__(test_case, context) do
171+
cond do
172+
function_exported?(test_case, :teardown_all, 1) ->
173+
test_case.teardown_all(context)
174+
function_exported?(test_case, :teardown_all, 0) ->
175+
test_case.teardown_all
176+
true ->
177+
context
178+
end
179+
end
180+
181+
@doc false
182+
def __setup_all__(test_case, context) do
183+
cond do
184+
function_exported?(test_case, :setup_all, 0) ->
185+
test_case.setup_all
186+
true ->
187+
context
188+
end
189+
end
190+
end

lib/ex_unit/lib/ex_unit/case.ex

Lines changed: 22 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,46 +9,35 @@ defmodule ExUnit.Case do
99
in parallel with others. Must be used for performance
1010
when your test cases do not change any global state;
1111
12-
## Callbacks
12+
This module automatically includes all callbacks defined
13+
in `ExUnit.Callbacks`. Read it for more information.
1314
14-
`ExUnit.Case` defines four callbacks:
15+
## Examples
1516
16-
* `setup_all()` and `teardown_all(context)` which are executed
17-
before and after all tests respectively;
18-
* `setup(context, test)` and `teardown(context, test)` which are
19-
executed before and after each test, receiving the test name
20-
as argument;
17+
defmodule AssertionTest do
18+
use ExUnit.Case, async: true
2119
22-
Such callbacks are useful to clean up any side-effect a test may cause,
23-
as for example, state in genservers, data on filesystem, or entries in
24-
a database. Data can be passed in between such callbacks as context,
25-
the context value returned by `setup_all` is passed down to all other
26-
callbacks. The value can then be updated in `setup` which is passed
27-
down to `teardown`.
28-
29-
## Examples
30-
31-
defmodule AssertionTest do
32-
use ExUnit.Case, async: true
33-
34-
def test_always_pass
35-
assert true
36-
end
37-
end
20+
def test_always_pass
21+
assert true
22+
end
23+
end
3824
3925
"""
4026

4127
@doc false
4228
defmacro __using__(opts // []) do
43-
if Keyword.get(opts, :async, false) do
44-
ExUnit.Server.add_async_case(__CALLER__.module)
45-
else
46-
ExUnit.Server.add_sync_case(__CALLER__.module)
47-
end
29+
async = Keyword.get(opts, :async, false)
4830

4931
quote do
32+
if unquote(async) do
33+
ExUnit.Server.add_async_case(__MODULE__)
34+
else
35+
ExUnit.Server.add_sync_case(__MODULE__)
36+
end
37+
5038
import ExUnit.Assertions
5139
import ExUnit.Case
40+
use ExUnit.Callbacks
5241
end
5342
end
5443

@@ -67,7 +56,7 @@ defmodule ExUnit.Case do
6756
end
6857
6958
"""
70-
defmacro test(message, contents) do
59+
defmacro test(message, var // quote(do: _), contents) do
7160
contents =
7261
case contents do
7362
[do: _] ->
@@ -84,12 +73,15 @@ defmodule ExUnit.Case do
8473

8574
quote do
8675
message = unquote(message)
76+
8777
message = if is_binary(message) do
8878
:"test #{message}"
8979
else
9080
:"test_#{message}"
9181
end
92-
def message, [], [], do: unquote(Macro.escape contents)
82+
83+
def message, [unquote(Macro.escape var)], [], do:
84+
unquote(Macro.escape contents)
9385
end
9486
end
9587
end

0 commit comments

Comments
 (0)