Combines EfLocalDb, TUnit, Verify.TUnit, and Verify.EntityFramework into a test base class that provides an isolated database per test with Arrange-Act-Assert phase enforcement.
https://nuget.org/packages/EfLocalDb.TUnit/
The snippets use a DbContext of the following form:
public class TheDbContext(DbContextOptions options) : DbContext(options)
{
public DbSet<Company> Companies { get; set; } = null!;
public DbSet<Employee> Employees { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder builder)
{
var company = builder.Entity<Company>();
company.HasKey(_ => _.Id);
company.HasMany(_ => _.Employees).WithOne(_ => _.Company).IsRequired();
var employee = builder.Entity<Employee>();
employee.HasKey(_ => _.Id);
employee.HasMany(_ => _.Vehicles).WithOne(_ => _.Employee).IsRequired();
var vehicle = builder.Entity<Vehicle>();
vehicle.HasKey(_ => _.Id);
}
}public class Company
{
public required Guid Id { get; init; }
public required string Name { get; set; }
public List<Employee> Employees { get; set; } = [];
}public class Employee
{
public required Guid CompanyId { get; set; }
public Company? Company { get; init; }
public required Guid Id { get; init; }
public required string Name { get; set; }
public List<Vehicle> Vehicles { get; set; } = [];
}LocalDbTestBase<T>.Initialize needs to be called once. This is best done in a ModuleInitializer:
public static class ModuleInitializer
{
[ModuleInitializer]
public static void Initialize()
{
VerifyDiffPlex.Initialize(OutputType.Compact);
VerifierSettings.InitializePlugins();
LocalDbLogging.EnableVerbose();
LocalDbSettings.ConnectionBuilder(_ => _.ConnectTimeout = 300);
LocalDbTestBase<TheDbContext>.Initialize();
}
}Inherit from LocalDbTestBase<T> and use the ArrangeData, ActData, and AssertData properties. These enforce phase ordering: accessing ActData transitions from Arrange to Act, and accessing AssertData transitions to Assert. Accessing a phase out of order throws an exception.
public class Tests :
LocalDbTestBase<TheDbContext>
{
[Test]
public async Task Simple()
{
ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "value"
});
await ArrangeData.SaveChangesAsync();
var entity = await ActData.Companies.SingleAsync();
entity.Name = "value2";
await ActData.SaveChangesAsync();
var result = await AssertData.Companies.SingleAsync();
await Verify(result);
}
[Test]
public async Task StaticInstance()
{
Instance.ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "value"
});
await Instance.ArrangeData.SaveChangesAsync();
var entity = await Instance.ActData.Companies.SingleAsync();
entity.Name = "value2";
await Instance.ActData.SaveChangesAsync();
var result = await Instance.AssertData.Companies.SingleAsync();
await Verify(result);
}
[Test]
public Task Combinations()
{
string[] inputs = ["value1", "value2"];
return Combination()
.Verify(Run, inputs);
async Task<Company> Run(string input)
{
ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "value"
});
await ArrangeData.SaveChangesAsync();
var entity = await ActData.Companies.SingleAsync();
entity.Name = input;
await ActData.SaveChangesAsync();
return await AssertData.Companies.SingleAsync();
}
}
[Test]
public Task Name() =>
Verify(new
{
Database.Name,
Database.Connection.DataSource
});
[Test]
[Arguments("case")]
public Task NameWithParams(string caseName) =>
Verify(new
{
Database.Name,
Database.Connection.DataSource
});
[Test]
public Task ThrowForRedundantIgnoreQueryFilters() =>
ThrowsTask(
() =>
{
var entities = AssertData.Companies;
return entities.IgnoreQueryFilters().SingleAsync();
})
.IgnoreStackTrace();
[Test]
public async Task IgnoreQueryFiltersAllowedOnArrangeAndAct()
{
await ArrangeData.Companies.IgnoreQueryFilters().ToListAsync();
await ActData.Companies.IgnoreQueryFilters().ToListAsync();
}
[Test]
public async Task ActInAsync()
{
ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "value"
});
await ArrangeData.SaveChangesAsync();
await AsyncMethod();
var result = await AssertData.Companies.SingleAsync();
await Verify(result);
async Task AsyncMethod()
{
await Task.Delay(1);
var entity = await ActData.Companies.SingleAsync();
entity.Name = "value2";
await ActData.SaveChangesAsync();
}
}
[Test]
public async Task VerifyEntityById()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "value"
};
ArrangeData.Companies.Add(company);
await ArrangeData.SaveChangesAsync();
await VerifyEntity<Company>(company.Id);
}
[Test]
public async Task VerifyEntityByIdNull() =>
await VerifyEntity<Company>(Guid.NewGuid());
[Test]
public async Task VerifyEntityWithInclude()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "the Company"
};
var employee = new Employee
{
Id = Guid.NewGuid(),
CompanyId = company.Id,
Name = "the Employee"
};
ArrangeData.AddRange(company, employee);
await ArrangeData.SaveChangesAsync();
await VerifyEntity<Company>(company.Id)
.Include(_ => _.Employees);
}
[Test]
public async Task VerifyEntityWithThenInclude()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "the Company"
};
var employee = new Employee
{
Id = Guid.NewGuid(),
CompanyId = company.Id,
Name = "the Employee"
};
var vehicle = new Vehicle
{
Id = Guid.NewGuid(),
EmployeeId = employee.Id,
Model = "the Vehicle"
};
ArrangeData.AddRange(company, employee, vehicle);
await ArrangeData.SaveChangesAsync();
await VerifyEntity<Company>(company.Id)
.Include(_ => _.Employees)
.ThenInclude(_ => _.Vehicles);
}
[Test]
public async Task VerifyEntities_DbSet()
{
ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "value"
});
await ArrangeData.SaveChangesAsync();
await VerifyEntities(AssertData.Companies);
}
[Test]
public async Task VerifyEntities_Queryable()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "value"
};
ArrangeData.Companies.Add(company);
await ArrangeData.SaveChangesAsync();
await VerifyEntities(AssertData.Companies.Where(_ => _.Id == company.Id));
}
[Test]
public async Task VerifyEntity_Queryable()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "value"
};
ArrangeData.Companies.Add(company);
await ArrangeData.SaveChangesAsync();
await VerifyEntity(AssertData.Companies.Where(_ => _.Id == company.Id));
}
[Test]
public async Task ArrangeQueryableAfterAct()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "value"
};
ArrangeData.Companies.Add(company);
await ArrangeData.SaveChangesAsync();
var queryable = ArrangeData.Companies.Where(_ => _.Id == company.Id);
// ReSharper disable once UnusedVariable
var act = ActData;
await ThrowsTask(() => VerifyEntities(queryable))
.IgnoreStackTrace()
.DisableRequireUniquePrefix();
}
[Test]
public Task AccessActAfterAssert()
{
// ReSharper disable once UnusedVariable
var assert = AssertData;
return Throws(() => ActData)
.IgnoreStackTrace();
}
[Test]
public async Task ActQueryableAfterAssert()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "value"
};
ArrangeData.Companies.Add(company);
await ArrangeData.SaveChangesAsync();
var queryable = ActData.Companies.Where(_ => _.Id == company.Id);
// ReSharper disable once UnusedVariable
var assert = AssertData;
await ThrowsTask(() => VerifyEntities(queryable))
.IgnoreStackTrace()
.DisableRequireUniquePrefix();
}
[Test]
public async Task ArrangeQueryableAfterAssert()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "value"
};
ArrangeData.Companies.Add(company);
await ArrangeData.SaveChangesAsync();
var queryable = ArrangeData.Companies.Where(_ => _.Id == company.Id);
// ReSharper disable once UnusedVariable
var assert = AssertData;
await ThrowsTask(() => VerifyEntities(queryable))
.IgnoreStackTrace()
.DisableRequireUniquePrefix();
}
[Test]
public Task AccessArrangeAfterAssert()
{
// ReSharper disable once UnusedVariable
var assert = AssertData;
return Throws(() => ArrangeData)
.IgnoreStackTrace();
}
[Test]
public Task AccessArrangeAfterAct()
{
// ReSharper disable once UnusedVariable
var act = ActData;
return Throws(() => ArrangeData)
.IgnoreStackTrace();
}
}The current test instance can be accessed via LocalDbTestBase<T>.Instance. This is useful when test helpers need to access the database outside the test class:
[Test]
public async Task StaticInstance()
{
Instance.ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "value"
});
await Instance.ArrangeData.SaveChangesAsync();
var entity = await Instance.ActData.Companies.SingleAsync();
entity.Name = "value2";
await Instance.ActData.SaveChangesAsync();
var result = await Instance.AssertData.Companies.SingleAsync();
await Verify(result);
}Verify Combinations are supported. The database is reset for each combination:
[Test]
public Task Combinations()
{
string[] inputs = ["value1", "value2"];
return Combination()
.Verify(Run, inputs);
async Task<Company> Run(string input)
{
ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "value"
});
await ArrangeData.SaveChangesAsync();
var entity = await ActData.Companies.SingleAsync();
entity.Name = input;
await ActData.SaveChangesAsync();
return await AssertData.Companies.SingleAsync();
}
}Helpers for verifying entities by primary key, with optional Include/ThenInclude:
[Test]
public async Task VerifyEntityById()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "value"
};
ArrangeData.Companies.Add(company);
await ArrangeData.SaveChangesAsync();
await VerifyEntity<Company>(company.Id);
}[Test]
public async Task VerifyEntityWithInclude()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "the Company"
};
var employee = new Employee
{
Id = Guid.NewGuid(),
CompanyId = company.Id,
Name = "the Employee"
};
ArrangeData.AddRange(company, employee);
await ArrangeData.SaveChangesAsync();
await VerifyEntity<Company>(company.Id)
.Include(_ => _.Employees);
}[Test]
public async Task VerifyEntityWithThenInclude()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "the Company"
};
var employee = new Employee
{
Id = Guid.NewGuid(),
CompanyId = company.Id,
Name = "the Employee"
};
var vehicle = new Vehicle
{
Id = Guid.NewGuid(),
EmployeeId = employee.Id,
Model = "the Vehicle"
};
ArrangeData.AddRange(company, employee, vehicle);
await ArrangeData.SaveChangesAsync();
await VerifyEntity<Company>(company.Id)
.Include(_ => _.Employees)
.ThenInclude(_ => _.Vehicles);
}Verify a collection of entities from a DbSet or IQueryable:
[Test]
public async Task VerifyEntities_DbSet()
{
ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "value"
});
await ArrangeData.SaveChangesAsync();
await VerifyEntities(AssertData.Companies);
}[Test]
public async Task VerifyEntity_Queryable()
{
var company = new Company
{
Id = Guid.NewGuid(),
Name = "value"
};
ArrangeData.Companies.Add(company);
await ArrangeData.SaveChangesAsync();
await VerifyEntity(AssertData.Companies.Where(_ => _.Id == company.Id));
}Mark test methods with [SharedDb] to share a single database across all query-only tests. Instead of cloning the template for each test, a shared database is created once and reused. This eliminates per-test DB creation overhead for tests that only read data.
Use [SharedDbWithTransaction] instead when tests need to write data. Each test runs inside an auto-rolling-back transaction, ensuring test isolation while still sharing the database instance.
Note: [SharedDbWithTransaction] means that on test failure the resulting database cannot be inspected (since the transaction is rolled back). A workaround when debugging a failure is to temporarily remove the attribute.
Both attributes can be mixed in the same test fixture:
public class SharedDbTests : LocalDbTestBase<TheDbContext>
{
[Test]
[SharedDb]
public async Task ReadFromSharedDb()
{
var count = await ActData.Companies.CountAsync();
await Assert.That(count).IsEqualTo(0);
}
[Test]
[SharedDbWithTransaction]
public async Task CanReadAndWrite()
{
ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "SharedDbWithTransaction Company"
});
await ArrangeData.SaveChangesAsync();
var entity = await ActData.Companies.SingleAsync();
await Assert.That(entity.Name)
.IsEqualTo("SharedDbWithTransaction Company");
}
[Test]
[SharedDbWithTransaction]
public async Task DataIsRolledBack()
{
ArrangeData.Companies.Add(
new()
{
Id = Guid.NewGuid(),
Name = "Should Not Persist"
});
await ArrangeData.SaveChangesAsync();
var count = await ActData.Companies.CountAsync();
await Assert.That(count).IsEqualTo(1);
}
[Test]
[SharedDbWithTransaction]
public async Task StartsWithEmptyDatabase()
{
var count = await ActData.Companies.CountAsync();
await Assert.That(count).IsEqualTo(0);
}
}To run tests in parallel, configure parallelism at the assembly level:
using TUnit.Core.Interfaces;
[assembly: ParallelLimiter<ParallelLimit2>]
public class ParallelLimit2 : IParallelLimit
{
public int Limit => 2;
}