Property Testing A Database Driver
A database driver is responsible for orchestrating the protocol between an application programming language and a database. Using Erlang/OTP and PostgreSQL, this article features writing property tests with PropEr to efficiently verify a driver implementation.
For example, PostgreSQL has smallint
which is
represented by a 16 bit signed integer. Using -1
as an example value, with a “traditional” non property based test:
smallint_test() ->
?assertEqual(-1, pbe("select $1::smallint", [-1])).
Where pbe
is a function that performs the actions of parse, bind
and execute. PostgreSQL
will parse select $1::smallint
and bind the parameter $1 with
-1
. When we execute this statement we assert that the database
responds with -1
. This mechanism enables us to test the
serialisation and deserialisation of each type.
As a signed 16 bit integer, the range of values for smallint
are
between -32,768 and 32,767. Using non property based testing methods,
we might write further tests that check that -32,768, …, -1, 0, 1, …,
32,767 are correctly represented:
smallint_test() ->
case for -32,768,
...
case for -1,
case for 0,
case for 1,
...
case for 32,767
There are dozens of other PostgreSQL types that also need testing. Maintaining the test data and verifying its correctness rapidly becomes a maintenance overhead.
Instead, with property based testing we define a “generator” for our
test inputs. For smallint
such a generator would be
integer(-32_768, 32_767)
. The tool uses this generator to create
valid test input, resulting in a property based test:
smallint_test() ->
?FORALL(
I,
integer(-32_768, 32_767),
begin
Result = pbe("select $1::smallint", [I]),
Result == I
end).
The above says that ?FORALL
integers (called I
) in the range of
-32,768..32,767, when executing select $1::smallint
with I
we
expect the Result
to be identical to I
.
We let the property based testing tool generate our test values using the model that we have supplied. The tool can be configured to generate as many test cases as we want using this model.
In PostgreSQL floating point numbers can be
represented by the real
or double precision
which are inexact,
variable-precision numeric types. The property test generator
float(inf, inf)
will generate floating point numbers between -∞ and
∞. However, real
has a precision of just 6 decimal
digits, whereas double precision
has 15. Rounding
might take place if the precision of an input number is too high. We
must ensure that the returned value is correct to the right level of
precision for the type that we are testing.
We can refine the generator using ?LET
for real
with 6 decimal
digits of precision:
?LET(F, float(inf, inf), precision(F, 6))
A value from the generator of float(inf, inf)
will be represented by
the variable F
. The call to precision(F, 6)
ensure that F
has
the appropriate number of decimal digits for the
type.
Our test for the real
now looks like:
real_test() ->
?FORALL(
Real,
?LET(F, float(inf, inf), precision(F, 6)),
begin
Result = pbe("select $1::real", [Real]),
Result == Real
end).
Further more complex examples can be found here, including PostgreSQL types of point, polygon, path, line segment, circle, box, line.