Audacia.Seed
is an open-source library for generating test data. It can be used to build objects in memory, as well as seeding test data into a repository. It has support for the following ORMs:
- Entity Framework Core (
Audacia.Seed.EntityFrameworkCore
) - Entity Framework (
Audacia.Seed.EntityFramework
)
The aim of this library is to reduce the overhead of setting up data for testing. It provides a fluent API for seeding data, and supports customisation of the data being seeded. This library has taken inspiration from libraries such as Bogus and AutoFixture, with the key difference of "Conservative seeding". That is, children will share a parent by default. Parents that are deemed to be optional will not be seeded unless otherwise specified.
This library is recommended for seeding complex data scenarios.
For even the simplest of data models, a lot of boilerplate code is required to seed a valid entity:
var booking = new Booking
{
//... set any required fields
Facility = new Facility
{
//... set any required fields
},
Member = new Member
{
//... set any required fields
}
};
_context.Set<Booking>().Add(booking);
_context.SaveChanges();
Audacia.Seed
removes this verbosity with the following:
_context.Seed<Booking>();
Similarly, if multiple entities need to be seeded:
const int amountOfBookingsToSeed = 5;
_context.SeedMany<Booking>(amountOfBookingsToSeed);
Inheritors of EntitySeed<TEntity>
are searched for using reflection. If none can be found, the library will attempt to create a valid instance of TEntity
on its own (see below).
The EntitySeed<TEntity>
has two important methods:
-
Prerequisites
: get the navigations that should be created as a predecessor toTEntity
, e.g a required parent. -
GetDefault
: create an instance ofTEntity
(without prerequisites).
When used together, these methods create a valid instance of TEntity
.
There are two ways that the library implements the above two methods to seed data.
Important
This is only supported in Audacia.Seed.EntityFrameworkCore
. For EF6, please see reflection-based-seeding below.
The library uses EF Core's IModel
interface to get information about what prerequisites TEntity
needs to be created - namely, it's required foreign keys. Once we've determined all required foreign keys, we'll construct a EntitySeed<TNavigation>
for it.
When seeding without a class, we figure out the Prerequisites
as follows:
- The library finds all fields that it thinks are navigation properties. This is based on the property being a
class
, and the existence of another property with the name{PropertyName}Id
. - A navigation property is considered as mandatory if its corresponding foreign key is not nullable.
- For each mandatory navigation property, we try to find a
EntitySeed<TNavigation>
class for it. If none is found, we'll repeat this process forTNavigation
.
When seeding without a class, the library will call the first constructor it finds for TEntity
, using example values for any constructor arguments as needed:
- if it is a
string
, a randomGuid
will be used. - otherwise, the default value for the type will be used.
The following table shows how each provider behaves when seeding data:
Provider | Model configuration | Reflection |
---|---|---|
EF Core | ✔️ | |
EF 6 | ️✔️️ | |
In-memory | ️️️✔️ |
Creating a child class of EntitySeed<TEntity>
class allows you to define a valid default TEntity
to be seeded looks like.
It should seed exactly the information required for an instance of TEntity
to be considered valid. For example, it may be that you have two optional navigation properties, but your domain ensures exactly one is populated. This is a good use-case for a custom class as the default behaviour will be to seed neither of these navigation properties.
You can create a class that inherits from EntitySeed<TEntity>
to represent a single valid instance of TEntity
as below:
public class MyEntitySeed : EntitySeed<MyEntity>
{
public override IEnumerable<ISeedPrerequisite> Prerequisites() =>
[
// This and `OptionalParentTwo` are optional FKs, but exactly one must be set for this entity to be valid.
new SeedPrerequisite<MyEntity, MyParent>(f => f.OptionalParentOne)
];
protected override MyEntity GetDefault(int index, MyEntity? previous)
{
return new MyEntity
{
Name = Guid.NewGuid().ToString()
};
}
}
Both the Prerequisites
and GetDefault
methods are optional. If you don't need to explicitly state what this behaviour will be, the library will behave as above.
Prerequisites
should be overridden if any mandatory properties don't follow the naming convention outlined above, or the navigation properties need to look a certain way by default. For example, a Booking
must have a Member
with a specific MembershipLevel
.
GetDefault
should be overridden if creating a new TEntity()
, and immediately saving throws errors that aren't related to foreign keys (e.g a required string
field). For example, a Coupon
must have a non-zero discount
.
Test data that is generated does not need to be saved to a database. The EntityBuilder
class can be used to generate test data in-memory. This is useful for unit tests that don't require a database, for example when testing extension methods.
var booking = new EntityBuilder().Build<Booking>();
Alternatively, you can provide a seed configuration to the EntityBuilder
:
var booking = new EntityBuilder().Build(new BookingSeed());
As well as the ability to build many:
var bookings = new EntityBuilder().BuildMany<Booking>(5);
Warning
Unlike EF Core / EF6, the reverse of navigation properties are not set when seeding in-memory. This means that if you populate a parent on a child, the collection of children on the parent will not necessarily be changed.
At a high level:
// When you want to override any default behaviour to specify a value that is meaningful to the test.
new BookingSeed().With(b => b.Name, "Booking Name");
// When you want to override any default behaviour to set a property as null.
new BookingSeed().Without(b => b.Name);
// When your database already contains the data you want to join up to.
new BookingSeed().WithExisting(b => b.Member);
// When you want each child to have a different parent.
new BookingSeed().WithDifferent(b => b.Member);
// When you want to specify an optional navigation property.
new BookingSeed().WithNew(b => b.Coupon);
// When you want to override the default behaviour for a mandatory navigation property.
new BookingSeed().WithNew(b => b.Member, new MemberSeed().With(m => m.FirstName, "Member Name"));
// When you're seeding from the parent level, but want to ensure the children are set up.
new MemberSeed().WithChildren(m => m.Bookings);
// When you want to control what the primary key is set to.
new MemberSeed().WithPrimaryKey(1);
Depending on whether you're seeding one or many entities, these customisations can then be seeded into the database context as follows:
// When you're seeding one entity
_context.Seed(seed);
// When you're seeding more than one entity
_context.SeedMany(5, seed);
Entities can be customised within the test via the following:
new BookingSeed()
.With(b => b.Name, "Booking Name")
.With(b => b.StartDate, new DateTime(2024, 1, 1));
Note
This can be also done for navigation properties so long as you provide a valid instance of the navigation property.
new BookingSeed().With(m => m.Member.FirstName, "Member Name");
new BookingSeed().With(m => m.Member, new Member() {...});
The following examples apply to when you're seeding multiple entities (i.e with SeedMany
).
new BookingSeed().With(b => b.Name, "Booking Name");
Note
In this scenario, all entities will have the same value for the property.
new BookingSeed().With(b => b.Name, "Booking A", "Booking B", "Booking C");
new CouponSeed().With(c => c.Discount, index => index * 10m);
new CouponSeed().With(c => c.Discount, (_, previous) => previous?.Discount * 2 ?? 0.1m)
new CouponSeed().With(
scs => scs.Name,
// Delegate is re-evaluated each time
() => Guid.NewGuid().ToString())
new BookingSeed().Without(b => b.Name);
Specify that an navigation optional property should be non-null, or override the default behaviour for a mandatory navigation property.
new BookingSeed().WithNew(b => b.Coupon);
In this scenario, we will look for inheritors of EntitySeed<Coupon>
, falling back on the default behaviour if none are found (see Reflection-based seeding).
new BookingSeed().WithNew(b => b.Coupon, new CouponSeed().With(...));
new BookingSeed().WithNew(b => b.Facility.Building);
Note
This isn't necessary if these relationships are mandatory.
If any parent in the property chain is optional, you will get a null reference exception. You can work around this as follows:
new BookingSeed()
.WithNew(b => b.OptionalParent)// This is necessary to ensure `OptionalParent` is not null before seeding further properties on it.
.WithNew(b => b.OptionalParent.Parent);
List<MemberSeed> memberSeeds = [ new MemberSeed(), new MemberSeed() ];
new BookingSeed().WithNew(
b => b.Coupon,
// First 3 bookings are for the first member
memberSeeds[0],
memberSeeds[0],
memberSeeds[0],
// Remaining 2 bookings are for the second member
memberSeeds[1],
memberSeeds[1]);
Note
If all you need is for the parents to be different, you can use WithDifferent instead.
new BookingSeed().WithNew(
// All bookings get the same member with the provided first name.
b => b.Member,
new MemberSeed().With(m => m.FirstName, "Member Name"));
You can only use WithDifferent
if you are seeding more than one entity. As such, the library will throw an exception if passed into Seed
rather than SeedMany
.
new BookingSeed().WithDifferent(b => b.Member);
new BookingSeed().WithDifferent(b => b.Member.MembershipGroup);
Use WithExisting
to seed an entity using data that already exists in the database. Use this when your database already has data in it.
new BookingSeed().WithExisting(b => b.Member);
new BookingSeed().WithExisting(b => b.Member, m => m.FirstName == "Member Name");
This will seed a single child Booking
for each Member
.
new MemberSeed().WithChildren(m => m.Bookings);
const int numberOfBookings = 5;
new MemberSeed().WithChildren(m => m.Bookings, numberOfBookings);
const int numberOfBookings = 2;
new MemberSeed().WithChildren(
m => m.Bookings,
numberOfBookings,
new BookingSeed().With(b => b.Name, "First Booking", "Second Booking"));
Note
You cannot use WithChildren
to join up to existing data in the database (like with WithExisting
). You can use the With
extension method to do this:
var existingBooking = _context.Bookings.First();
new MemberSeed().With(m => m.Bookings, [existingBooking]);
Consider the following interface:
public interface IHasName
{
string Name { get; set; }
}
You can specify a customisation to set these fields automatically as follows:
public class SeedNamesCustomisation<TEntity> : ISeedCustomisation<TEntity>
where TEntity : class, IHasName
{
public void Apply(TEntity entity, ISeedableRepository repository, int index, TEntity? previous)
{
entity.Name = Guid.NewGuid().ToString();
}
}
This customisation can then be applied to any entity that implements IHasName
using the AddCustomisation
method:
new PersonSeed().AddCustomisation(new SeedNamesCustomisation<Person>())
And will therefore automatically set the Name
property of all Member
s.
var building = _context.Seed<Building>();
var seed = new BookingSeed()
.WithDifferent(b => b.Facility)
.With(b => b.Facility.BuildingId, building.Id, null, building.Id);
const int bookingsToSeed = 3;
var bookings = _context.SeedMany(bookingsToSeed, seed).ToList();
// Seeds the following:
// > Facility 1 (in Building 1)
// > Booking 1
// > Facility 2 (no building)
// > Booking 2
// > Facility 3 (in Building 1)
// > Booking 3
var facilitySeeds = new[] { new FacilitySeed(), new FacilitySeed() };
var seed = new BookingSeed()
.WithNew(b => b.Facility, facilitySeeds[0], facilitySeeds[0], facilitySeeds[1]);
const int bookingsToSeed = 3;
_context.SeedMany(bookingsToSeed, seed);
// Seeds the following:
// > Facility 1
// > Booking 1
// > Booking 2
// > Facility 2
// > Booking 3
var start = DateTime.Now;
var finish = start.AddHours(1);
var seed = new BookingSeed()
.With(b => b.Start, start)
.With(b => b.Finish, finish)
.WithDifferent(b => b.Facility.Building);
const int amountToCreate = 2;
var bookings = _context.SeedMany(amountToCreate, seed).ToList();
// Seeds the following:
// > Building 1
// > Facility 1
// > Booking 1 (Start: Now, Finish: Now + 1 hour)
// > Building 2
// > Facility 2
// > Booking 2 (Start: Now, Finish: Now + 1 hour)
// Employee A manages Facility A and Facility B
// Employee B manages Facility C and Owns Facility A
// Employee C owns Facility B and Facility C
var employeeSeeds = new[]
{
new EmployeeSeed().With(e => e.FirstName, "A"),
new EmployeeSeed().With(e => e.FirstName, "B"),
new EmployeeSeed().With(e => e.FirstName, "C")
};
var seed = new FacilitySeed()
.With(f => f.Name, "A", "B", "C")
.WithNew(f => f.Owner, employeeSeeds[1], employeeSeeds[2], employeeSeeds[2])
.WithNew(f => f.Manager, employeeSeeds[0], employeeSeeds[0], employeeSeeds[1]);
const int amountToCreate = 3;
_context.SeedMany(amountToCreate, seed);
// Seeds the following:
// > Facility A (Owned by Employee B, Managed by Employee A)
// > Facility B (Owned by Employee C, Managed by Employee A)
// > Facility C (Owned by Employee C, Managed by Employee B)
const int bookingsPerMember = 2;
var seed = new MemberSeed()
.WithChildren(m => m.Bookings, bookingsPerMember);
const int amountOfMembers = 2;
_context.SeedMany(amountOfMembers, seed);
// Seeds the following:
// > Member 1
// > Booking 1
// > Booking 2
// > Member 2
// > Booking 3
// > Booking 4
By default, the library will look for seeds in the same assembly as the test. If you have a separate project for your seeds, you can specify this assembly attribute in your test project as follows:
[assembly: SeedAssembly("My.Seed.Assembly")]
See here for an example.
We don't know at compile time how many entities you want to seed vs how many entities your customisation will handle. As such, the following code will build successfully:
var seed = new BookingSeed()
.With(b => b.Name, "Booking A", "Booking B");
_context.SeedMany(3, seed);
In this scenario, a DataSeedingException
will be thrown. This will also happen if you provide 3 values, but only 2 entities are being seeded.
Note
You can still provide a single value for a property, and seed multiple entities. In this scenario, all entities will have the same value for the property.
When seeding many children, the default behaviour will be to share a parent.
_context.SeedMany<Booking>(5);
_context.Set<Booking>.Count();// 5 Bookings
_context.Set<Facility>.Count();// 1 Facility for all Bookings
_context.Set<Member>.Count();// 1 Member for all Bookings
This is also true for optional navigations.
To override this behaviour, use WithDifferent:
_context.SeedMany(
5,
new BookingSeed()
.WithDifferent(b => b.Member)
.WithDifferent(b => b.Facility));
_context.Set<Booking>.Count();// 5 Bookings
_context.Set<Facility>.Count();// 5 Facilities, one for each Booking
_context.Set<Member>.Count();// 5 Members, one for each Booking
If you need to seed data for a complex graph of objects with multiple branches, a good way to approach this is to work out the nearest entity which all branches will seed. The seed configuration for that entity will be reused by default, so you can use one seed configuration per branch, and they should all join up at that entity.
For example, consider this data model:
- A
Booking
has a singleMember
. - A
Member
has manyContact
s, which has manyAddress
es.
It is difficult to seed everything from the Address
level, because it is dependent on the Member
, but the Member
is the principal in the other relationships being seeded, so you end up seeding from the principal end of the relationship.
Instead, provide one seed configuration for each branch of the data model. In this case, we have two branches: Booking
and Address
. Both of them will require a Member
, and the functionality will reuse the same Member
for both of them.
_context.Seed(new AddressSeed(), new BookingSeed());
It is most performant to seed everything in a single save. However, for complex data setups it's recommended to split out the seeding in stages to improve readability.
Sometimes you need complicated seed data, and can't work out how to set it all up using the EntitySeed
classes. In some scenarios, this may be made easier by seeding the data in two stages.
If a specific test in the class is more complex than others, consider seeding the data for that test from within the test, rather than the constructor:
public class MyTests
{
public MyTests()
{
// Seed simple data here that all tests need
}
[Fact]
public void MySimpleUnitTests()
{
// No addititional setup required
}
[Fact]
public void MyComplexUnitTests()
{
// Additional commplex seeding goes here
}
}
The Audacia.Seed
repository change history can be found in this changelog:
We welcome contributions! Please feel free to check our Contribution Guidlines for feature requests, issue reporting and guidelines.