From the original Haskell version,
QuickCheck is a library for random testing of program properties. The programmer provides a specification of the program, in the form of properties which functions should satisfy, and QuickCheck then tests that the properties hold in a large number of randomly generated cases.
This library aims to provide similar property testing functionality for .NET.
QuickCheck is available as a NuGet package. You can add it to an existing project as you would any other public nuget package. Using the .NET CLI:
dotnet add package QuickCheck
Say I've written a function to reverse Memory<int>
. A property of a working reverse function is that reversing a reversed sequence should give the original sequence. We can check that this is the case for our function using QuickCheck.
using QuickCheck;
// The function to test
static Memory<T> Reverse<T>(Memory<T> memory)
{
var span = memory.Span;
var reversed = new T[span.Length];
for (var i = 0; i < span.Length; i++)
{
reversed[span.Length - i - 1] = span[i];
}
return new Memory<T>(reversed);
}
var qc = QuickChecker.CreateEmpty();
// Create and add a generator for `Memory<int>`
var generator = new ArbitraryMemoryGenerator<int>(
ArbitraryInt32Generator.Default,
Random.Shared,
maximumSize: 32);
qc.AddGenerator(generator);
// Test function and validate that for all Memory<int> (Reverse(Reverse(memory)) == memory)
var result = qc.Run(
target: static (Memory<int> memory) => Reverse(Reverse(memory)).Span.SequenceEqual(memory.Span),
validate: static (result) => result);
Console.WriteLine(result.Type); // Success
The main entry point for interacting with the library is the QuickChecker
class.
To construct an instance with the default generators pre-registered, use an overload of the CreateDefault
method.
using QuickCheck;
// Using the default options
var qc = QuickChecker.CreateDefault();
// Using custom options
var configuredQc = QuickChecker.CreateDefault(options =>
{
options.RunCount = 1000;
options.Random = new Random(42);
});
To construct an instance with no pre-registered generators, use one of the CreateEmpty
overloads.
using QuickCheck;
// Using the default options
var qc = QuickChecker.CreateEmpty();
// Using custom options
var configuredQc = QuickChecker.CreateEmpty(options =>
{
options.RunCount = 1000;
options.Random = new Random(42);
});
To test your functions, the QuickChecker
must be able to generate values for every argument type of the target function.
This library provides a number of built-in generators for common types, including many numeric types, char
, and string
. These are added when using CreateDefault
overloads.
Generic generators are available for List<T>
, Memory<T>
, and tuples, when given a generator for T
. These must be added manually.
To add a generator to the QuickChecker
instance, use the AddGenerator
method:
using QuickCheck.Generators;
var qc = QuickChecker.CreateEmpty();
qc.AddGenerator(ArbitraryInt32Generator.Default);
With the QuickChecker
configured, you can test a function by calling the Run
method.
Say I wrote a method to divide two integers, but return 0 if the divisor is 0. However, I forgot to handle the case where the divisor is 0.
using QuickCheck;
// If b == 0, return 0, else return a / b. Oops!
static int NotSoSafeDivide(int a, int b) => a / b;
var qc = QuickChecker.CreateDefault();
var result = qc.Run(static (int a, int b) => NotSoSafeDivide(a, b));
Console.WriteLine(result.IsError); // True
Console.WriteLine(result.Exception is DivideByZeroException); // True
The original QuickCheck library in Haskell:
I learned about QuickCheck from Jon Gjengset's great video on the Rust implementation: