Skip to content

Commit b665ddd

Browse files
authored
Add parameterized tests (#13618)
Sometimes you want to run the same tests but with different parameters. In ExUnit, it is possible to do so by passing a `:parameterize` key to `ExUnit.Case`. The value must be a list of maps which will be the parameters merged into the test context. For example, Elixir has a module called `Registry`, which can have type `:unique` or `:duplicate`, and can control its concurrency factor using the `:partitions` option. If you have a number of tests that *behave the same* across all of those values, you can parameterize those tests with: use ExUnit.Case, async: true, parameterize: for(kind <- [:unique, :duplicate], partitions <- [1, 8], do: %{kind: kind, partitions: partitions}) Then, in your tests, you can access the parameters as part of the context: test "starts a registry", %{kind: kind, partitions: partitions} do ... end Use parameterized tests with care: * Although parameterized tests run concurrently when `async: true` is also given, abuse of parameterized tests may make your test suite slower * If you use parameterized tests and then find yourself adding conditionals in your tests to deal with different parameters, then parameterized tests may be the wrong solution to your problem. Consider creating separated tests and sharing logic between them using regular functions
1 parent a7cd364 commit b665ddd

File tree

10 files changed

+929
-861
lines changed

10 files changed

+929
-861
lines changed

lib/elixir/test/elixir/registry_test.exs

Lines changed: 735 additions & 779 deletions
Large diffs are not rendered by default.

lib/ex_unit/lib/ex_unit.ex

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,9 +101,10 @@ defmodule ExUnit do
101101
* `:time` - the duration in microseconds of the test's runtime
102102
* `:tags` - the test tags
103103
* `:logs` - the captured logs
104+
* `:parameters` - the test parameters
104105
105106
"""
106-
defstruct [:name, :case, :module, :state, time: 0, tags: %{}, logs: ""]
107+
defstruct [:name, :case, :module, :state, time: 0, tags: %{}, logs: "", parameters: %{}]
107108

108109
# TODO: Remove the `:case` field on v2.0
109110
@type t :: %__MODULE__{
@@ -131,8 +132,10 @@ defmodule ExUnit do
131132
132133
* `:tests` - all tests in this module
133134
135+
* `:parameters` - the test module parameters
136+
134137
"""
135-
defstruct [:file, :name, :state, tags: %{}, tests: []]
138+
defstruct [:file, :name, :state, tags: %{}, tests: [], parameters: %{}]
136139

137140
@type t :: %__MODULE__{
138141
file: binary(),
@@ -404,10 +407,12 @@ defmodule ExUnit do
404407
for module <- additional_modules do
405408
module_attributes = module.__info__(:attributes)
406409

407-
if true in Keyword.get(module_attributes, :ex_unit_async, []) do
408-
ExUnit.Server.add_async_module(module)
409-
else
410-
ExUnit.Server.add_sync_module(module)
410+
case Keyword.get(module_attributes, :ex_unit_module) do
411+
[config] ->
412+
ExUnit.Server.add_module(module, config)
413+
414+
_ ->
415+
raise(ArgumentError, "#{inspect(module)} is not a ExUnit.Case module")
411416
end
412417
end
413418

lib/ex_unit/lib/ex_unit/case.ex

Lines changed: 67 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ defmodule ExUnit.Case do
1515
* `:register` - when `false`, does not register this module within
1616
ExUnit server. This means the module won't run when ExUnit suite runs.
1717
18+
* `:parameterize` - a list of maps to parameterize tests. If both
19+
`:async` and `:parameterize` are given, the different parameters
20+
run concurrently. See the "Parameterized tests" section below for
21+
more information.
22+
1823
> #### `use ExUnit.Case` {: .info}
1924
>
2025
> When you `use ExUnit.Case`, it will import the functionality
@@ -173,6 +178,41 @@ defmodule ExUnit.Case do
173178
174179
* `:tmp_dir` - (since v1.11.0) see the "Tmp Dir" section below
175180
181+
## Parameterized tests
182+
183+
Sometimes you want to run the same tests but with different parameters.
184+
In ExUnit, it is possible to do so by passing a `:parameterize` key to
185+
`ExUnit.Case`. The value must be a list of maps which will be the
186+
parameters merged into the test context.
187+
188+
For example, Elixir has a module called `Registry`, which can have type
189+
`:unique` or `:duplicate`, and can control its concurrency factor using
190+
the `:partitions` option. If you have a number of tests that *behave the
191+
same* across all of those values, you can parameterize those tests with:
192+
193+
use ExUnit.Case,
194+
async: true,
195+
parameterize:
196+
for(kind <- [:unique, :duplicate],
197+
partitions <- [1, 8],
198+
do: %{kind: kind, partitions: partitions})
199+
200+
Then, in your tests, you can access the parameters as part of the context:
201+
202+
test "starts a registry", %{kind: kind, partitions: partitions} do
203+
...
204+
end
205+
206+
Use parameterized tests with care:
207+
208+
* Although parameterized tests run concurrently when `async: true` is also given,
209+
abuse of parameterized tests may make your test suite slower
210+
211+
* If you use parameterized tests and then find yourself adding conditionals
212+
in your tests to deal with different parameters, then parameterized tests
213+
may be the wrong solution to your problem. Consider creating separated
214+
tests and sharing logic between them using regular functions
215+
176216
## Filters
177217
178218
Tags can also be used to identify specific tests, which can then
@@ -278,6 +318,18 @@ defmodule ExUnit.Case do
278318
~s(got: #{inspect(opts)})
279319
end
280320

321+
{register?, opts} = Keyword.pop(opts, :register, true)
322+
{async?, opts} = Keyword.pop(opts, :async, false)
323+
{parameterize, opts} = Keyword.pop(opts, :parameterize, nil)
324+
325+
unless parameterize == nil or (is_list(parameterize) and Enum.all?(parameterize, &is_map/1)) do
326+
raise ArgumentError, ":parameterize must be a list of maps, got: #{inspect(parameterize)}"
327+
end
328+
329+
if opts != [] do
330+
IO.warn("unknown options given to ExUnit.Case: #{inspect(opts)}")
331+
end
332+
281333
registered? = Module.has_attribute?(module, :ex_unit_tests)
282334

283335
unless registered? do
@@ -299,23 +351,18 @@ defmodule ExUnit.Case do
299351

300352
Enum.each(accumulate_attributes, &Module.register_attribute(module, &1, accumulate: true))
301353

302-
persisted_attributes = [:ex_unit_async]
354+
persisted_attributes = [:ex_unit_module]
303355

304356
Enum.each(persisted_attributes, &Module.register_attribute(module, &1, persist: true))
305357

306-
if Keyword.get(opts, :register, true) do
358+
if register? do
307359
Module.put_attribute(module, :after_compile, ExUnit.Case)
308360
end
309361

310362
Module.put_attribute(module, :before_compile, ExUnit.Case)
311363
end
312364

313-
async? = opts[:async]
314-
315-
if is_boolean(async?) or not registered? do
316-
Module.put_attribute(module, :ex_unit_async, async? || false)
317-
end
318-
365+
Module.put_attribute(module, :ex_unit_module, {async?, parameterize})
319366
registered?
320367
end
321368

@@ -498,21 +545,22 @@ defmodule ExUnit.Case do
498545
end
499546

500547
@doc false
501-
defmacro __before_compile__(env) do
548+
defmacro __before_compile__(%{module: module} = env) do
502549
tests =
503-
env.module
550+
module
504551
|> Module.get_attribute(:ex_unit_tests)
505552
|> Enum.reverse()
506553
|> Macro.escape()
507554

508-
moduletag = Module.get_attribute(env.module, :moduletag)
555+
moduletag = Module.get_attribute(module, :moduletag)
556+
{async?, _parameterize} = Module.get_attribute(module, :ex_unit_module)
509557

510558
tags =
511559
moduletag
512560
|> normalize_tags()
513561
|> validate_tags()
514562
|> Map.new()
515-
|> Map.merge(%{module: env.module, case: env.module})
563+
|> Map.merge(%{module: module, case: env.module, async: async?})
516564

517565
quote do
518566
def __ex_unit__ do
@@ -529,17 +577,16 @@ defmodule ExUnit.Case do
529577
@doc false
530578
def __after_compile__(%{module: module}, _) do
531579
cond do
532-
Process.whereis(ExUnit.Server) == nil ->
533-
unless Code.can_await_module_compilation?() do
534-
raise "cannot use ExUnit.Case without starting the ExUnit application, " <>
535-
"please call ExUnit.start() or explicitly start the :ex_unit app"
536-
end
580+
Process.whereis(ExUnit.Server) ->
581+
config = Module.get_attribute(module, :ex_unit_module)
582+
ExUnit.Server.add_module(module, config)
537583

538-
Module.get_attribute(module, :ex_unit_async) ->
539-
ExUnit.Server.add_async_module(module)
584+
Code.can_await_module_compilation?() ->
585+
:ok
540586

541587
true ->
542-
ExUnit.Server.add_sync_module(module)
588+
raise "cannot use ExUnit.Case without starting the ExUnit application, " <>
589+
"please call ExUnit.start() or explicitly start the :ex_unit app"
543590
end
544591
end
545592

@@ -577,7 +624,6 @@ defmodule ExUnit.Case do
577624

578625
moduletag = Module.get_attribute(mod, :moduletag)
579626
tag = Module.delete_attribute(mod, :tag)
580-
async = Module.get_attribute(mod, :ex_unit_async)
581627

582628
{name, describe, describe_line, describetag} =
583629
case Module.get_attribute(mod, :ex_unit_describe) do
@@ -602,7 +648,6 @@ defmodule ExUnit.Case do
602648
line: line,
603649
file: file,
604650
registered: registered,
605-
async: async,
606651
describe: describe,
607652
describe_line: describe_line,
608653
test_type: test_type

lib/ex_unit/lib/ex_unit/cli_formatter.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,14 @@ defmodule ExUnit.CLIFormatter do
137137
{:noreply, update_test_timings(config, test)}
138138
end
139139

140-
def handle_cast({:module_started, %ExUnit.TestModule{name: name, file: file}}, config) do
140+
def handle_cast({:module_started, %ExUnit.TestModule{} = module}, config) do
141141
if config.trace do
142+
%{name: name, file: file, parameters: parameters} = module
142143
IO.puts("\n#{inspect(name)} [#{Path.relative_to_cwd(file)}]")
144+
145+
if parameters != %{} do
146+
IO.puts("Parameters: #{inspect(parameters)}")
147+
end
143148
end
144149

145150
{:noreply, config}

lib/ex_unit/lib/ex_unit/formatter.ex

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ defmodule ExUnit.Formatter do
9494
| :error_info
9595
| :test_module_info
9696
| :test_info
97+
| :parameters_info
9798
| :location_info
9899
| :stacktrace_info
99100
| :blame_diff
@@ -119,7 +120,7 @@ defmodule ExUnit.Formatter do
119120
* `:diff_insert` and `:diff_insert_whitespace` - Should format a diff insertion,
120121
with or without whitespace respectively.
121122
122-
* `:extra_info` - Should format extra information, such as the `"code: "` label
123+
* `:extra_info` - Should format optional extra labels, such as the `"code: "` label
123124
that precedes code to show.
124125
125126
* `:error_info` - Should format error information.
@@ -129,6 +130,8 @@ defmodule ExUnit.Formatter do
129130
130131
* `:test_info` - Should format test information.
131132
133+
* `:parameters_info` - Should format test parameters.
134+
132135
* `:location_info` - Should format test location information.
133136
134137
* `:stacktrace_info` - Should format stacktrace information.
@@ -266,9 +269,10 @@ defmodule ExUnit.Formatter do
266269
) :: String.t()
267270
when failure: {atom, term, Exception.stacktrace()}
268271
def format_test_failure(test, failures, counter, width, formatter) do
269-
%ExUnit.Test{name: name, module: module, tags: tags} = test
272+
%ExUnit.Test{name: name, module: module, tags: tags, parameters: parameters} = test
270273

271274
test_info(with_counter(counter, "#{name} (#{inspect(module)})"), formatter) <>
275+
test_parameters(parameters, formatter) <>
272276
test_location(with_location(tags), formatter) <>
273277
Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, index} ->
274278
{text, stack} = format_kind_reason(test, kind, reason, stack, width, formatter)
@@ -305,9 +309,10 @@ defmodule ExUnit.Formatter do
305309
) :: String.t()
306310
when failure: {atom, term, Exception.stacktrace()}
307311
def format_test_all_failure(test_module, failures, counter, width, formatter) do
308-
name = test_module.name
312+
%{name: name, parameters: parameters} = test_module
309313

310314
test_module_info(with_counter(counter, "#{inspect(name)}: "), formatter) <>
315+
test_parameters(parameters, formatter) <>
311316
Enum.map_join(Enum.with_index(failures), "", fn {{kind, reason, stack}, index} ->
312317
{text, stack} = format_kind_reason(test_module, kind, reason, stack, width, formatter)
313318
failure_header(failures, index) <> text <> format_stacktrace(stack, name, nil, formatter)
@@ -711,6 +716,15 @@ defmodule ExUnit.Formatter do
711716
defp test_info(msg, nil), do: msg <> "\n"
712717
defp test_info(msg, formatter), do: test_info(formatter.(:test_info, msg), nil)
713718

719+
defp test_parameters(params, _formatter) when params == %{}, do: ""
720+
defp test_parameters(params, nil) when is_binary(params), do: " " <> params <> "\n"
721+
722+
defp test_parameters(params, nil) when is_map(params),
723+
do: test_parameters("Parameters: #{inspect(params)}", nil)
724+
725+
defp test_parameters(params, formatter),
726+
do: test_parameters(formatter.(:parameters_info, params), nil)
727+
714728
defp test_location(msg, nil), do: " " <> msg <> "\n"
715729
defp test_location(msg, formatter), do: test_location(formatter.(:location_info, msg), nil)
716730

0 commit comments

Comments
 (0)