Skip to content

Commit 4b32394

Browse files
authored
Add param_feature to wrap Wallaby's feature macro (#21)
1 parent 8a7a431 commit 4b32394

10 files changed

Lines changed: 192 additions & 64 deletions

File tree

.github/workflows/elixir-build-and-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
build-flags: --all-warnings --warnings-as-errors
3636

3737
- name: Run Tests
38-
run: mix coveralls.json --warnings-as-errors
38+
run: mix coveralls.json --warnings-as-errors --include feature --include integration
3939
if: always()
4040

4141
# Optional, but Codecov has a bot that will comment on your PR with per-file

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Changelog
22

3+
## v0.4.0
4+
5+
- Adds a new `param_feature` macro, which wraps Wallaby's `feature` tests
6+
the same way `param_test` wraps ExUnit's `test`.
7+
8+
(While you _can_ use the plain `param_test` macro in a test module that
9+
contains `use Wallaby.Feature`, doing so will break some Wallaby features
10+
including screenshot generation on failure.)
11+
- Moves the `parse_examples/2` function, an implementation detail for the
12+
`param_test` macro, into a new private module `ParameterizedTest.Parser`.
13+
314
## v0.3.1
415

516
Bug fix to accept more unquoted strings, including those that have Elixir delimiters in them like quotes, parentheses, etc.

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ param_test "grants free shipping based on the marketing site's stated policy",
107107
end
108108
```
109109

110+
The package also provides a second macro, `param_feature`, which wraps
111+
Wallaby's `feature` tests the same way `param_test` wraps ExUnit's `test`.
112+
(While you _can_ use the plain `param_test` macro in a test module that
113+
contains `use Wallaby.Feature`, doing so will break some Wallaby features
114+
including screenshot generation on failure.)
115+
110116
## Why parameterized testing?
111117

112118
Parameterized testing reduces toil associated with writing tests that cover

config/runtime.exs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import Config
2+
3+
if Mix.env() == :test do
4+
config :wallaby,
5+
driver: Wallaby.Chrome,
6+
screenshot_on_failure: true
7+
end

lib/parameterized_test.ex

Lines changed: 95 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ NimbleCSV.define(ParameterizedTest.TsvParser, separator: "\t", escape: "\"")
22
NimbleCSV.define(ParameterizedTest.CsvParser, separator: ",", escape: "\"")
33

44
defmodule ParameterizedTest do
5-
@moduledoc ~S"""
5+
@moduledoc """
66
A utility for defining eminently readable parameterized (or example-based) tests.
77
88
Parameterized tests look like this:
@@ -14,7 +14,7 @@ defmodule ParameterizedTest do
1414
| %{shoes: 19_99, pants: 29_99} | | false | Spent too little |
1515
| %{shoes: 59_99, pants: 49_99} | | true | Spent $100+ |
1616
| %{socks: 10_99} | | true | Socks ship free |
17-
| %{pants: 1_99} | "FREE_SHIP" | true | Correct coupon |
17+
| %{pants: 1_99} | \"FREE_SHIP\" | true | Correct coupon |
1818
\"\"\",
1919
%{
2020
spending_by_category: spending_by_category,
@@ -82,63 +82,37 @@ defmodule ParameterizedTest do
8282
8383
Use it like:
8484
85-
param_test "works as expected", your_parameters, %{value: from_context, expected_result: expected_result} do
86-
assert MyModule.process(from_context) == expected_result
85+
param_test \"grants free shipping for spending $99+ or with coupon FREE_SHIP\",
86+
\"\"\"
87+
| total_cents | ships_free? | description |
88+
| ----------- | ----------- | --------------------------- |
89+
| 98_99 | false | Spent too little |
90+
| 99_00 | true | Min for free shipping |
91+
| 99_01 | true | Spent more than the minimum |
92+
\"\"\",
93+
%{total_cents: total_cents, ships_free?: ships_free?} do
94+
shipping_cost = ShippingCalculator.calculate(total_cents)
95+
96+
if ships_free? do
97+
assert shipping_cost == 0
98+
else
99+
assert shipping_cost > 0
100+
end
87101
end
102+
88103
"""
89104
defmacro param_test(test_name, examples, context_ast \\ %{}, blocks) do
90105
context = Macro.Env.location(__ENV__)
91-
92-
escaped_examples =
93-
case examples do
94-
str when is_binary(str) ->
95-
file_extension =
96-
str
97-
|> Path.extname()
98-
|> String.downcase()
99-
100-
case file_extension do
101-
ext when ext in [".md", ".markdown", ".csv"] ->
102-
str
103-
|> Parser.parse_file_path_examples(context)
104-
|> Macro.escape()
105-
106-
_ ->
107-
str
108-
|> Parser.parse_examples(context)
109-
|> Macro.escape()
110-
end
111-
112-
list when is_list(list) ->
113-
list
114-
|> Parser.parse_examples(context)
115-
|> Macro.escape()
116-
117-
already_escaped when is_tuple(already_escaped) ->
118-
already_escaped
119-
end
106+
escaped_examples = escape_examples(examples, context)
120107

121108
quote location: :keep do
122109
for {example, index} <- Enum.with_index(unquote(escaped_examples)) do
123110
for {key, val} <- example do
124111
@tag [{key, val}]
125112
end
126113

127-
custom_description =
128-
example[:test_desc] || example[:test_description] || example[:description] || example[:Description]
129-
130-
un_truncated_name =
131-
case custom_description do
132-
nil -> "#{unquote(test_name)} (#{inspect(example)})"
133-
test_name -> "#{unquote(test_name)} - #{test_name}"
134-
end
135-
136-
full_test_name =
137-
cond do
138-
String.length(un_truncated_name) <= 212 -> un_truncated_name
139-
is_nil(custom_description) -> "#{unquote(test_name)} row #{index}"
140-
true -> String.slice(un_truncated_name, 0, 212)
141-
end
114+
unquoted_test_name = unquote(test_name)
115+
full_test_name = ParameterizedTest.Parser.full_test_name(unquoted_test_name, example, index, 212)
142116

143117
@tag param_test: true
144118
test "#{full_test_name}", unquote(context_ast) do
@@ -147,4 +121,77 @@ defmodule ParameterizedTest do
147121
end
148122
end
149123
end
124+
125+
if Code.ensure_loaded?(Wallaby) do
126+
@doc """
127+
Defines Wallaby feature tests that use your parameters or example data.
128+
129+
This is to the Wallaby `feature` macro as `param_test` is to `test`.
130+
131+
Use it like this:
132+
133+
param_feature \"supports Wallaby tests\",
134+
\"\"\"
135+
| text | url |
136+
|----------|----------------------|
137+
| \"GitHub\" | \"https://github.com\" |
138+
| \"Google\" | \"https://google.com\" |
139+
\"\"\",
140+
%{session: session, text: text, url: url} do
141+
session
142+
|> visit(url)
143+
|> assert_has(Wallaby.Query.text(text, minimum: 1))
144+
end
145+
"""
146+
defmacro param_feature(test_name, examples, context_ast \\ %{}, blocks) do
147+
context = Macro.Env.location(__ENV__)
148+
escaped_examples = escape_examples(examples, context)
149+
150+
quote location: :keep do
151+
for {example, index} <- Enum.with_index(unquote(escaped_examples)) do
152+
for {key, val} <- example do
153+
@tag [{key, val}]
154+
end
155+
156+
unquoted_test_name = unquote(test_name)
157+
@full_test_name ParameterizedTest.Parser.full_test_name(unquoted_test_name, example, index, 212)
158+
159+
@tag param_test: true
160+
feature "#{@full_test_name}", unquote(context_ast) do
161+
unquote(blocks)
162+
end
163+
end
164+
end
165+
end
166+
end
167+
168+
defp escape_examples(examples, context) do
169+
case examples do
170+
str when is_binary(str) ->
171+
file_extension =
172+
str
173+
|> Path.extname()
174+
|> String.downcase()
175+
176+
case file_extension do
177+
ext when ext in [".md", ".markdown", ".csv"] ->
178+
str
179+
|> Parser.parse_file_path_examples(context)
180+
|> Macro.escape()
181+
182+
_ ->
183+
str
184+
|> Parser.parse_examples(context)
185+
|> Macro.escape()
186+
end
187+
188+
list when is_list(list) ->
189+
list
190+
|> Parser.parse_examples(context)
191+
|> Macro.escape()
192+
193+
already_escaped when is_tuple(already_escaped) ->
194+
already_escaped
195+
end
196+
end
150197
end

lib/parameterized_test/parser.ex

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
defmodule ParameterizedTest.Parser do
22
@moduledoc false
3-
43
@typep context :: [{:line, integer} | {:file, String.t()}]
54

65
@spec parse_examples(String.t() | list, context()) :: [map()]
@@ -28,6 +27,41 @@ defmodule ParameterizedTest.Parser do
2827
end
2928
end
3029

30+
@spec parse_file_path_examples(String.t(), context()) :: [map()]
31+
def parse_file_path_examples(path, context) do
32+
file = File.read!(path)
33+
34+
case path |> Path.extname() |> String.downcase() do
35+
md when md in [".md", ".markdown"] -> parse_examples(file, context)
36+
".csv" -> parse_csv_file(file, context)
37+
".tsv" -> parse_tsv_file(file, context)
38+
_ -> raise "Unsupported file extension for parameterized tests #{path} #{file_meta(context)}"
39+
end
40+
end
41+
42+
@spec full_test_name(String.t(), map(), integer, integer) :: String.t()
43+
def full_test_name(original_test_name, example, row_index, max_chars) do
44+
custom_description = description(example)
45+
46+
un_truncated_name =
47+
case custom_description do
48+
nil -> "#{original_test_name} (#{inspect(example)})"
49+
desc -> "#{original_test_name} - #{desc}"
50+
end
51+
52+
cond do
53+
String.length(un_truncated_name) <= max_chars -> un_truncated_name
54+
is_nil(custom_description) -> "#{original_test_name} row #{row_index}"
55+
true -> String.slice(un_truncated_name, 0, max_chars)
56+
end
57+
end
58+
59+
defp description(%{test_description: desc}), do: desc
60+
defp description(%{test_desc: desc}), do: desc
61+
defp description(%{description: desc}), do: desc
62+
defp description(%{Description: desc}), do: desc
63+
defp description(_), do: nil
64+
3165
defp parse_hand_rolled_table(evaled_table, context) do
3266
parsed_table = Enum.map(evaled_table, &Map.new/1)
3367

@@ -45,18 +79,6 @@ defmodule ParameterizedTest.Parser do
4579
parsed_table
4680
end
4781

48-
@spec parse_file_path_examples(String.t(), context()) :: [map()]
49-
def parse_file_path_examples(path, context) do
50-
file = File.read!(path)
51-
52-
case path |> Path.extname() |> String.downcase() do
53-
md when md in [".md", ".markdown"] -> parse_examples(file, context)
54-
".csv" -> parse_csv_file(file, context)
55-
".tsv" -> parse_tsv_file(file, context)
56-
_ -> raise "Unsupported file extension for parameterized tests #{path} #{file_meta(context)}"
57-
end
58-
end
59-
6082
defp parse_csv_file(file, context) do
6183
file
6284
|> ParameterizedTest.CsvParser.parse_string(skip_headers: false)

mix.exs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ defmodule ParameterizedTest.MixProject do
66
def project do
77
[
88
app: :parameterized_test,
9-
version: "0.3.1",
9+
version: "0.4.0",
1010
elixir: "~> 1.14",
1111
elixirc_paths: elixirc_paths(Mix.env()),
1212
start_permanent: Mix.env() == :prod,
@@ -77,6 +77,8 @@ defmodule ParameterizedTest.MixProject do
7777
defp deps do
7878
List.flatten(
7979
[
80+
# Optional: supports doing parameterization over Wallaby `feature` tests
81+
{:wallaby, ">= 0.0.0", optional: true},
8082
{:nimble_csv, "~> 1.1"},
8183
{:ex_doc, ">= 0.0.0", only: :dev, runtime: false},
8284

0 commit comments

Comments
 (0)