The Pollution library creates streams of values with potentially complex data types. It is used to support the Quixir property-based testing library.
Pollution contains a number of value generators. These generate streams of values of a particular type. To make them interesting, they can be configured, both to constrain the values they produce and also the subtypes that may be contained in those values.
For example:
list
generates a stream of lists. Each list will be a random size, and will contain random values of any type
int
generates a stream of integers
list(of: int)
generates random length lists, and every element will be an integer
list(of: int(min: 5, max: 21))
random length lists of integers between 5 and 21
list(of: choose(int, float), min: 1, max: 4))
lists of between 1 and 4 elements, where each contains either all integers or all floats.
Value generators can be composed to arbitrary depths, but be careful of combinatorial explosions of size:
list(list(list(list(list))))
could potentially generate 10 billion values per result.
Add to your dependencies:
def deps do
[
{ :pollution, "~> 0.1.0" }
]
end
The value generator functions live in module Pollution.VG
. Either
alias it and prefix the function names with VG
:
defmodule MyModule do
alias Pollution.VG
def my_function do
IO.inspect VG.list(VG.int) |> Enum.take(5)
end
end
or import it and pollute your module with dozens of fun value generators 😄
defmodule MyModule do
import Pollution.VG
def my_function do
IO.inspect list(tuple(atom, list(int))) |> Enum.take(5)
end
end
Most VGs support the option to force certain values to appear in the
result stream. This facility is used in the Quixir testing framework
to ensure that common boundary values are tried alongside more
esoteric values. For example, a stream of integers will start -1
,
0
, 1
, and the continue with more random values. A stream of
strings will start ""
, "⊔"
, a stream of lists with an empty list,
and so on.
These are called must have values, and each VG has its own.
You can set your own must have values for a VG:
int(must_have: [0, 1, 99, 12345])
list(must_have: [ [], [ nil ], [ false ] ])
A VG will try to include must have values. However, it also checks any
other constraints you may have applied, and alter the must have list
accordingly. For example, the default must have list for integers is
[-1, 0, 1]
:
iex> int |> Enum.take(5)
[ -1, 0, 1, 42, -88484 ]
If we constrain the integer to have a minimum value of zero, the must have list changes:
iex> int(min: 0) |> Enum.take(5)
[ 0, 1, 42, 663732, 967 ]
You can constrain the must have values away altogether:
iex> int(min: 5, max: 7) |> Enum.take(5)
[ 6, 7, 5, 5, 6 ]
This also applies to must have values you set yourself.