Fluent Test Scaffold is intended to help with setting up test data for Integration Tests. It allows you to build a Test Scaffold using a Fluent API of predefined builders to seed data or initialise services.
block-beta
columns 1
UI["<b>System Testing</b><br/>E2E, UI, Acceptance Testing (Blackbox Testing)"]
INT["<b>Integration Testing</b><br/>Fluent Test Scaffold (Whitebox Testing)"]
COMP["<b>Component Testing</b><br/>Individual Components (Mocked dependencies)"]
UNIT["<b>Unit Testing</b><br/>Isolated Functions"]
style UI fill:#ffcdd2,stroke:#000,stroke-width:2px,color:#000,width:500px
style INT fill:#c8e6c9,stroke:#000,stroke-width:2px,color:#000,width:500px
style COMP fill:#fff3e0,stroke:#000,stroke-width:2px,color:#000,width:500px
style UNIT fill:#e1f5fe,stroke:#000,stroke-width:2px,color:#000,width:500px
The Testing Pyramid is a concept that describes the different levels of testing in software development. It emphasizes the importance of having a solid foundation of unit tests, followed by integration tests, and finally end-to-end (E2E) tests. Generally, the higher up the pyramid you go, the more complex and time-consuming the tests become.
Integration tests fit in the middle where they provide more realistic coverage but are often difficult to set up and maintain. Fluent Test Scaffold aims to simplify the integration testing process by providing a structured way to set up your test environment and dependencies.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#ffffff','primaryTextColor':'#000000','primaryBorderColor':'#000000','lineColor':'#000000','secondaryColor':'#f0f0f0','tertiaryColor':'#ffffff'}}}%%
flowchart LR
subgraph "Traditional Component Testing"
MOCK[Mock Dependencies<br/>❌ Complex Setup<br/>❌ Brittle Tests<br/>❌ False Confidence]
ISOLATED[Isolated Components<br/>❌ Missing Integration Bugs<br/>❌ Mock Drift Issues]
end
subgraph "FluentTestScaffold Integration Testing"
REAL[Real Dependencies<br/>âś… Actual Database<br/>âś… Real Services<br/>âś… True Integration]
FLUENT[Fluent API<br/>âś… Easy Setup<br/>âś… Readable Tests<br/>âś… Consistent Data]
SCAFFOLD[Test Scaffold<br/>âś… Reusable Builders<br/>âś… Data Templates<br/>âś… IOC Management]
REAL --> FLUENT
FLUENT --> SCAFFOLD
end
MOCK -.->|"FluentTestScaffold<br/>Eliminates"| REAL
ISOLATED -.->|"Replaced by"| SCAFFOLD
style MOCK fill:#ffcdd2,stroke:#000,stroke-width:2px,color:#000
style ISOLATED fill:#ffcdd2,stroke:#000,stroke-width:2px,color:#000
style REAL fill:#c8e6c9,stroke:#000,stroke-width:2px,color:#000
style FLUENT fill:#e8f5e8,stroke:#000,stroke-width:2px,color:#000
style SCAFFOLD fill:#e3f2fd,stroke:#000,stroke-width:2px,color:#000
Integration Tests are often mistaken for Unit tests. Integration Tests focus on testing the interaction between components that together make up a more complicated workflow. They can often be difficult to create due to their dependencies that often lead to external integrations like a Database. Due to these complexities we often mock these services instead. This can lead to both false positives and false negatives unless you mock the service to respond exactly the same as it would normally.
The Test Scaffold aims to assist with the setup your services in an environment that matches how it would run normally. This includes using an IOC container to register and resolve dependencies and a Database Builders to help initialise your database state.
By setting up the Data Structures in your tests the same way that they are used in production, we can initialize any dependant services in the same manner or with minimal mocking to give a more realistic representation of how your application would run.
# Install the core package
dotnet add package FluentTestScaffold.Core
# Install framework-specific package
dotnet add package FluentTestScaffold.EntityFrameworkCore| Package | Description |
|---|---|
FluentTestScaffold.Core |
Core framework and interfaces |
FluentTestScaffold.AspNetCore |
ASP.NET Core integration |
FluentTestScaffold.Autofac |
Autofac DI container support |
FluentTestScaffold.Bdd |
BDD testing support |
FluentTestScaffold.EntityFrameworkCore |
Entity Framework Core support |
FluentTestScaffold.Nunit |
NUnit testing framework support |
[Test]
public void ComponentIntegrationTest_UserCanAddToCart()
{
using var dbContext = TestDbContextFactory.Create();
var email = "Jim@test.com";
var password = "SupperSecretPa$$word";
// Arrange
var userId = Guid.Parse("A5A743C3-A02F-4CA3-94F8-B0ECAF4A6345");
var testScaffold = new TestScaffold()
.UseAutofac(ctx =>
{
ctx.Container.Register(_ => dbContext).SingleInstance();
ctx.Container.RegisterType<AuthService>().As<IAuthService>();
ctx.Container.Register(c =>
{
var requestContext = new UserRequestContext(c.Resolve<IAuthService>());
requestContext.AuthenticateUser(email, password);
return requestContext;
})
.As<IUserRequestContext>()
.InstancePerLifetimeScope();
ctx.Container.RegisterType<ShoppingCartService>();
})
.UsingBuilder<InventoryBuilder>()
.WithTemplate<ApplicationDataTemplates>(dt => dt.DefaultCatalogueAndUsers())
.WithDefaultCatalogue()
.With(new User(
id: userId,
email: email,
password: password,
name: "Jimmy",
dateOfBirth: DateOnly.FromDateTime(DateTime.Now.AddYears(-8))
))
.WithShoppingCart(userId)
.Build();
var item = dbContext.Items.FirstOrDefault(i => i.Title == Defaults.CatalogueItems.DeadPool);
// Act
var shoppingCartService = testScaffold.Resolve<ShoppingCartService>();
shoppingCartService.AddItemToCart(item!.Id);
// Assert
var cart = dbContext.ShoppingCart.Include(s => s.Inventory).FirstOrDefault(u => u.UserId == userId);
Assert.IsTrue(cart?.Inventory.Any(i => i.Id == item.Id));
}%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#ffffff','primaryTextColor':'#000000','primaryBorderColor':'#000000','lineColor':'#000000','secondaryColor':'#f0f0f0','tertiaryColor':'#ffffff'}}}%%
flowchart TB
subgraph "FluentTestScaffold Core Architecture"
TS[TestScaffold<br/>Entry Point] --> IOC[IOC Container<br/>.NET/Autofac]
TS --> TSC[TestScaffoldContext<br/>Shared State]
TS --> DT[Data Templates<br/>Preset Configurations]
IOC --> B1[Builder A<br/>Database Setup]
IOC --> B2[Builder B<br/>Service Config]
IOC --> B3[Builder N<br/>Custom Logic]
B1 --> TSC
B2 --> TSC
B3 --> TSC
DT --> B1
DT --> B2
DT --> B3
TSC --> TEST[Your Integration Test]
IOC --> TEST
end
subgraph "Fluent API Flow"
START[new TestScaffold] --> SETUP[UseIoc/UseAutofac]
SETUP --> BUILDER[UsingBuilder T]
BUILDER --> ACTIONS[With/WithRange/Custom]
ACTIONS --> BUILD[Build]
BUILD --> RESOLVE[Resolve Services]
RESOLVE --> EXECUTE[Execute Test]
end
style TS fill:#e1f5fe,stroke:#000,stroke-width:2px,color:#000
style IOC fill:#f3e5f5,stroke:#000,stroke-width:2px,color:#000
style TSC fill:#e8f5e8,stroke:#000,stroke-width:2px,color:#000
style DT fill:#fff3e0,stroke:#000,stroke-width:2px,color:#000
style TEST fill:#ffebee,stroke:#000,stroke-width:2px,color:#000
style B1 fill:#e8f5e8,stroke:#000,stroke-width:2px,color:#000
style B2 fill:#e8f5e8,stroke:#000,stroke-width:2px,color:#000
style B3 fill:#e8f5e8,stroke:#000,stroke-width:2px,color:#000
style START fill:#e3f2fd,stroke:#000,stroke-width:2px,color:#000
style SETUP fill:#fff3e0,stroke:#000,stroke-width:2px,color:#000
style BUILDER fill:#e8f5e8,stroke:#000,stroke-width:2px,color:#000
style ACTIONS fill:#f3e5f5,stroke:#000,stroke-width:2px,color:#000
style BUILD fill:#ffebee,stroke:#000,stroke-width:2px,color:#000
style RESOLVE fill:#e1f5fe,stroke:#000,stroke-width:2px,color:#000
style EXECUTE fill:#c8e6c9,stroke:#000,stroke-width:2px,color:#000
FluentTestScaffold provides a fluent API that enables method chaining to build complex test scenarios with minimal setup code. The API follows a logical flow from initialization through configuration to execution.
// Start with TestScaffold constructor
var testScaffold = new TestScaffold()The TestScaffold class serves as the main entry point and orchestrates the entire fluent API workflow.
// .NET Default IOC Container
.UseIoc(services => {
services.AddSingleton<IMyService, MyService>();
services.AddTransient<IRepository, Repository>();
})
// Autofac IOC Container
.UseAutofac(ctx => {
ctx.Container.RegisterType<MyService>().As<IMyService>();
ctx.Container.Register(_ => dbContext).SingleInstance();
})Key Methods:
UseIoc(Action<IServiceCollection>)- Configure .NET's built-in dependency injectionUseAutofac(Action<AutofacServiceBuilder>)- Configure Autofac container with advanced features
// Switch to specific builders for data setup
.UsingBuilder<UserBuilder>()
.With(new User { Name = "John", Email = "john@test.com" })
.WithRange(users)
.UsingBuilder<ProductBuilder>()
.WithDefaultProducts()
.With(new Product { Name = "Widget", Price = 10.99m })Key Methods:
UsingBuilder<T>()- Resolve and switch to a specific builder typeWith(entity)- Add a single entity to the test contextWithRange(entities)- Add multiple entities at onceWithDefault*()- Apply predefined default data sets (custom builder methods likeWithDefaultProducts())
// Apply predefined data configurations
.WithTemplate<ApplicationDataTemplates>(dt => dt.DefaultCatalogueAndUsers())
.WithTemplate<ApplicationDataTemplates>(dt => dt.SetupUserWithOrders(userId))Key Methods:
WithTemplate<TTemplate>(dt => dt.MethodName())- Apply a strongly-typed data templateWithTemplate<TTemplate>(dt => dt.MethodName(param1, param2))- Apply template with parameters
// Finalize setup and resolve services
.Build();
// Resolve services from IOC container
var userService = testScaffold.Resolve<IUserService>();
var repository = testScaffold.Resolve<IUserRepository>();Key Methods:
Build()- Finalize the test scaffold configuration and build the IOC containerResolve<T>()- Get services from the configured IOC container
%%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#ffffff','primaryTextColor':'#000000','primaryBorderColor':'#000000','lineColor':'#000000','secondaryColor':'#f0f0f0','tertiaryColor':'#ffffff'}}}%%
flowchart LR
NEW[new TestScaffold] --> IOC[UseIoc/UseAutofac]
IOC --> BUILDER[UsingBuilder T]
BUILDER --> DATA[With/WithRange/WithDefault]
DATA --> TEMPLATE[WithTemplate]
TEMPLATE --> BUILD[Build]
BUILD --> RESOLVE[Resolve T]
BUILDER -.->|"Switch Context"| BUILDER2[UsingBuilder U]
BUILDER2 --> DATA2[With/WithRange]
DATA2 --> TEMPLATE
style NEW fill:#e3f2fd,stroke:#000,stroke-width:2px,color:#000
style IOC fill:#fff3e0,stroke:#000,stroke-width:2px,color:#000
style BUILDER fill:#e8f5e8,stroke:#000,stroke-width:2px,color:#000
style BUILDER2 fill:#e8f5e8,stroke:#000,stroke-width:2px,color:#000
style DATA fill:#f3e5f5,stroke:#000,stroke-width:2px,color:#000
style DATA2 fill:#f3e5f5,stroke:#000,stroke-width:2px,color:#000
style TEMPLATE fill:#e1f5fe,stroke:#000,stroke-width:2px,color:#000
style BUILD fill:#ffebee,stroke:#000,stroke-width:2px,color:#000
style RESOLVE fill:#c8e6c9,stroke:#000,stroke-width:2px,color:#000
// Behavior-Driven Development style API
testScaffold
.Scenario("User can add items to cart")
.Given(scaffold => scaffold.UsingBuilder<UserBuilder>().WithDefaultUser())
.When(scaffold => {
var cartService = scaffold.Resolve<ICartService>();
cartService.AddItem(productId);
})
.Then(scaffold => {
var cart = scaffold.Resolve<ICartRepository>().GetCart(userId);
Assert.IsTrue(cart.Items.Any(i => i.ProductId == productId));
});BDD Methods:
Scenario(description)- Define test scenario descriptionGiven(setup)- Setup test preconditionsWhen(action)- Execute the action under testThen(assertion)- Verify expected outcomes
The TestScaffoldContext allows sharing data between builders, templates, and tests:
// In a builder
Context.Set("UserId", userId);
// In another builder or test
var userId = Context.Get<Guid>("UserId");var testScaffold = new TestScaffold()
.UseAutofac(ctx => {
ctx.Container.Register(_ => dbContext).SingleInstance();
ctx.Container.RegisterType<UserService>().As<IUserService>();
})
.UsingBuilder<UserBuilder>()
.WithTemplate<ApplicationDataTemplates>(dt => dt.SetupStandardUser())
.With(new User { Email = "test@example.com" })
.UsingBuilder<ProductBuilder>()
.WithDefaultProducts()
.Build();
var userService = testScaffold.Resolve<IUserService>();
// Execute test logic...Fluent Test Scaffold was designed to be framework independent to allow for extensibility.
The internal IOC can be replaced with an IOC framework that supports standard .net IServiceProviderFactory
The Base Builder class, while intended to be used to build up a Database is DB independent. A builder could technically be used to build other context required for your tests, such as a file.
The current implementation supports .net & Autofac for IOC and Entity Framework Core for the Database Builders.
This project uses GitHub Actions for continuous integration and deployment with release tag-based versioning. The CI/CD pipeline includes:
-
CI (
ci.yml): Runs on pushes to main and pull requests- Builds and tests on multiple .NET versions (6.0, 7.0, 8.0)
- Runs tests with code coverage collection
- Generates HTML coverage reports
- Enforces minimum code coverage (90%)
- Uploads coverage artifacts for download
- Runs security analysis and code quality checks
-
Release Publishing (
release-publish.yml): Runs on published releases- Extracts version from release tag (e.g.,
v1.0.0) - Validates release is from main branch
- Builds and validates packages with tag-based versioning
- Publishes to both NuGet.org and GitHub Packages
- Updates release notes with publishing information
- Supports test mode with preview versions
- Extracts version from release tag (e.g.,
-
CodeQL: Automated security scanning
- Runs static analysis for security vulnerabilities
- Integrated with GitHub Security tab
To use the CI/CD pipeline, you need to configure:
NUGET_API_KEY: Your NuGet.org API key for publishing packagesGITHUB_TOKEN: Automatically provided by GitHub Actions
-
Create a release from main branch:
# Ensure you're on main branch git checkout main git pull origin main # Create and push release tag git tag v1.0.0 git push origin v1.0.0 # Create GitHub release (triggers release-publish workflow) gh release create v1.0.0 --title "Release v1.0.0" --notes "Release notes here"
-
The CI/CD pipeline will:
- Validate the release is from main branch
- Build all packages with version extracted from tag
- Run all tests and security scans
- Publish to both NuGet.org and GitHub Packages
- Update release notes with publishing information
For detailed CI/CD setup instructions, see CI/CD Setup Guide.
- Getting Started - Step-by-step guide to the main features
- Setup
- CI/CD Setup - Continuous Integration and Deployment setup
- Code Coverage - Code coverage setup and usage guide
- Ioc - IOC container used by the TestScaffold.
- Builders - builders are used to build up the Test Scaffold context
- Data Templates - builders are used to build up the Test Scaffold context
- Test Context - provides a place to share state between Tests, Builders and Data Templates
- Config & Auto Discovery - builders are used to build up the Test Scaffold context
- FluentTestScaffold.Bdd - Adds BDD style fluent api to the TestScaffold
- ASP.Net Core - Write Controller Integration Tests including the full ASP.net stack for Auth Tests