Awesome
EntroBuilder and EntroTester
What are they?
EntroBuilder
Is a library that facilitates generation of random test data. Through a fluent interface it allows to define generation strategies for certain types, or for a specific property explicitly within an object graph.
Combining randomness and tests, creates the risk of having non-deterministic tests. For this reason, the default random seed used is a fixed value and it is configurable. As such, consecutive test runs should yield the same results.
EntroTester
Is a library that leverages EntroBuilder
for automated blackbox testing. Random testing allows you to discover branches in your code which do not have expected behavior.
This technique is also known as Fuzz testing:
Fuzz testing or fuzzing is a software testing technique, often automated or semi-automated, that involves providing invalid, unexpected, or random data to the inputs of a computer program. The program is then monitored for exceptions such as crashes, or failing built-in code assertions, or for finding potential memory leaks.
The idea is that EntroTester
provides a random seed to EntroBuilder
to verify failure of certain assertions. In case a failure is detected, an exception is thrown with the random seed, and iteration at which the failure occurred. This enables the developer to replay and debug the failure scenario.
Installation
The packages EntroBuilder
, EntroTester
and their dependency FARE
are hosted in the NuGet Gallery.
To install the packages from the package manager console. (EntroTester automatically includes EntroBuilder)
Install-Package EntroBuilder
If you just want the test data builder.
Install-Package EntroTester
If you want the entire package.
Concepts
IGenerator<T>
EntroBuilder
relies on a concept called IGenerator<T>
:
interface IGenerator<T>
{
T Next(Random random);
}
Similar to IEnumerator<T>
, IGenerator<T>
produces a sequence of Ts
, be it that the latter is considered to be an infinite sequence. Some IGenerator<T>
implementations use a finite collection to produce values, however when the internal sequence has reached the end, it wraps around, and starts from the beginning.
This interface is the core of the library, and any and all types are constructed in this fashion.
EntroBuilder
offers a number of different generators:
ScalarGenerator<T>
is a base type for all the primitive generators. There are type generators for most primitives as well as some complex types:bool
,byte
,DateTime
,decimal
,double
,float
,Guid
,short
,int
,long
,string
as well as theirNullable<T>
counterparts.SequenceGenerator<T>
takes anIEnumerable<T>
and produces uniformly distributed random values from the sequence. The issue withIEnumerable<T>
is that it can represent an infinite sequence, and since we want to produce uniformly distributed values, we need to determine whether it's a finite or infinite sequence.RangeGenerator<T>
takes an upper and lower bound and produces random values between the two bounds. Implementations exist for most countable primitives.RegexGenerator
takes a regular expression and leverages FARE to produce random strings that match the regex pattern.
Builder<T>
The Builder is a class that does the actual work of binding a Type to a IGenerator<T>
. It equally offers a way to override the default generation strategy via For()
and Property()
:
For<T>(IGenerator<T>)
allows to override the default generation strategy for given type. All instances of given type will be generated using this generator.Property<TProperty>(Expression<Func<T, TProperty>>, IGenerator<TProperty>)
allows to override the generation strategy for a specific property within the object graph.
Usage
IGenerator<T>
The factory class Any
allows to easily produce instances of the aforementioned generators. Following methods are available:
Any.ValueIn<T>(params T[]) // Produces a SequenceGenerator<T>
Any.ValueBetween(int, int) // Produces a RangeGenerator<int>
Any.ValueLike(string) // Produces a RegexGenerator
The factory class Is
allows to produce a generator for a static instance.
Is.Value<T>(T) // Produces a SequenceGenerator<T> with one element.
TODO: Elaborate on CustomGenerator<T>
Builder<T>
Creating an instance
Every instance of test data can be constructed using the Builder Factory method:
var builder = Builder.Create<int>();
The builder can produce instances of int
by either calling Build()
, either Take(int)
.
var myInteger = builder.Build(); // Produces an Integer instance
var myIntegers = builder.Take(10); // Produces 10 Integer instances
Overriding default generation strategy for specific type
The following creates an Order
for which all properties of type int
will have a value between 0
and 10
.
var order = Builder.Create<Order>()
.For<int>(Any.ValueBetween(0, 10))
.Build();
Overriding generation strategy for specific property
The following creates a Customer
for which the FirstName
will either be John, Bob or Will, and the LastName
will either be Appleseed, Builder or Shakespear.
var order = Builder.Create<Order>()
.Property(
o => o.Customer.FullName.FirstName,
Any.ValueIn("John", "Bob", "Will"))
.Property(
o => o.Customer.FullName.LastName,
Any.ValueIn("Appleseed", "Builder", "Shakespear"))
.Build();
The following creates an Order
for which the OrderLines
will have an Amount between 0.01 and 1.99.
var order = Builder.Create<Order>()
.Property(
o => o.OrderLines.Select(ol => ol.Amount),
Any.ValueBetween(0.01M, 1.99M))
.Build();
Overriding type mapping
When working with interface or base class properties, it is necessary to provide EntroBuilder with hints for which concrete type to instantiate.
interface ICustomer { }
class MyCustomer : ICustomer { }
class Order
{
public ICustomer Customer { get; set; }
}
var order = Builder.Create<Order>()
.For<ICustomer, MyCustomer>()
.Build();
Fallback generation strategy for any type not supported out-of-the box or by the user
Whenever the generation strategy cannot be expressed statically, or a convention-based rule is more fitting in your context you can use the Builder<T>.Configure(...)
directive to configure custom behavior.
interface ICustomer { }
class MyCustomer : ICustomer { }
interface IProduct { }
class MyProduct : IProduct { }
class Order
{
public ICustomer Customer { get; set; }
public IProduct Product { get; set; }
}
// This IFallbackGenerator scans the interface assembly for a concrete class that implements the interface.
// DefaultInterfaceImplementationFallbackGenerator can be constructed with a list of assemblies
// if the assemblies to be scanned is different from the assembly containing the interfaces
var order = Builder.Create<Order>()
.Configure(new Builder.Configuration { FallbackGenerator = new DefaultInterfaceImplementationFallbackGenerator() })
.Build();