Current section
Files
Jump to
Current section
Files
lib/quick_chex.ex
defmodule QuickChex do
@moduledoc """
main module with the main macros that you can use when writing property-based
tests.
add `use QuickChex` at the top of your ExUnit test module
## Example
defmodule MyModuleTest do
use ExUnit.Case, async: true
use QuickChex # <- add this!
end
"""
import QuickChex.Generators, warn: false
@default_iterations_number Application.get_env(:quick_chex, :iterations, 100)
@doc false
defmacro __using__(_opts) do
quote do
import unquote(__MODULE__)
import QuickChex.Generators
ExUnit.plural_rule("property", "properties")
Module.register_attribute __MODULE__, :qc_properties, accumulate: true
end
end
@doc """
specify a property with a single generate parameter
A property has a `name` to be identified by checks, a `param1` that will
be generated, and a body to specify the requirements
## Example
for a **negate** property of a `Test.negate/1` function you could write a
property like this:
```
property :negate, value do
assert not value === Test.negate(value)
end
property :negate_two_time_means_doing_nothing, value do
res = value
|> Test.negate
|> Test.negate
assert value === res
end
```
"""
defmacro property(name, param1, do: contents) do
contents = Macro.escape(contents)
param1 = Macro.escape(param1)
quote bind_quoted: [name: name, param1: param1, contents: contents] do
@qc_properties name
func_name = "quick_chex_property_#{name}" |> String.to_atom
def unquote(func_name)(unquote(param1)) do
unquote(contents)
end
end
end
@doc """
same as property/2 with two generated parameters
## Example
a property for the Test.add/2 function
```
property :add_is_commutative, n1, n2 do
assert Test.add(n1, n2) === Test.add(n2, n1)
end
```
"""
defmacro property(name, param1, param2, do: contents) do
contents = Macro.escape(contents)
param1 = Macro.escape(param1)
param2 = Macro.escape(param2)
quote bind_quoted: [name: name, param1: param1, param2: param2,
contents: contents] do
@qc_properties name
func_name = "quick_chex_property_#{name}" |> String.to_atom
def unquote(func_name)(unquote(param1), unquote(param2)) do
unquote(contents)
end
end
end
@doc """
same as property/2 with three generated parameters
"""
defmacro property(name, param1, param2, param3, do: contents) do
contents = Macro.escape(contents)
param1 = Macro.escape(param1)
param2 = Macro.escape(param2)
param3 = Macro.escape(param3)
quote bind_quoted: [name: name, param1: param1, param2: param2,
param3: param3, contents: contents] do
@qc_properties name
func_name = "quick_chex_property_#{name}" |> String.to_atom
def unquote(func_name)(unquote(param1), unquote(param2),
unquote(param3)) do
unquote(contents)
end
end
end
@doc """
check a property by giving the property name and a list of settings
settings:
* **with** a *list* of generators (or values) to be passed to the property.
**Be careful!** the number of generators **must** match the number of
arguments of the property
* **iterations** *integer* representing the number of iterations
* **only_if** a *function* with the same arity as the number of generators
in the with settings.
If the function resturns true than the generated values are considered
acceptable for the property, otherwise another attempt is made until a
suitable array of values is found. Use this with caution, as it could slow
down your test suite considerably.
*a full example*
check :my_property,
with: [non_neg_integer, non_neg_integer],
itarations: 100,
only_if: fn num1, num2 -> num1 > num2 end
this checks a property with two non negative integer args. The *only_if*
function ensures that the first number is greater than the second one.
usually there is a better approach
check :my_property,
with: [{:non_neg_integer, [0, 1000], {:non_neg_integer, [1001, 2000]}],
itarations: 100
by better specifying the generators we can omit the only_if block and speed up
our test suite. Use the only_if block only if generator constraints are not
enough.
"""
defmacro check(name, check_name \\ nil, settings) do
generators = settings
|> Keyword.get(:with)
|> Macro.escape
iterations = settings[:iterations] || @default_iterations_number
only_if = settings[:only_if]
quote bind_quoted: [name: name, check_name: check_name, settings: settings,
generators: generators, iterations: iterations, only_if: only_if] do
func_name = "quick_chex_property_#{name}" |> String.to_atom
1..iterations
|> Enum.map(fn num ->
args = calculate_args(generators, only_if)
test_func_name = ExUnit.Case.register_test(__ENV__, :property,
register_name(name, check_name, num), [[args: args]])
args = args |> Macro.escape
def unquote(test_func_name)(_) do
apply(__MODULE__, unquote(func_name), unquote(args))
end
end)
end
end
@doc false
def register_name(name, nil, iteration_num) do
"#{name} - iteration #{iteration_num}"
end
def register_name(name, check_name, iteration_num) do
"#{name} - #{check_name} - iteration #{iteration_num}"
end
@doc false
def calculate_args(generators = {:fn, _, _}, _) do
{func, _} = Code.eval_quoted(generators, [], __ENV__)
func.()
end
def calculate_args(generators, nil) do
{values, _} = Code.eval_quoted(generators, [], __ENV__)
values
end
def calculate_args(generators, check_function) do
do_calculate_args(:next, generators, check_function)
end
defp do_calculate_args({:ok, values}, _, _), do: values
defp do_calculate_args(:next, generators, check_function) do
{values, _} = Code.eval_quoted(generators, [], __ENV__)
res = if apply(check_function, values) do
{:ok, values}
else
:next
end
do_calculate_args(res, generators, check_function)
end
end