| name | xunit-testing-patterns |
| description | Master xUnit testing patterns for ABP Framework applications including unit tests, integration tests, test data seeders, and mocking strategies. Use when: (1) writing xUnit tests for ABP services, (2) creating test data seeders, (3) implementing integration tests, (4) setting up test infrastructure. |
| layer | 3 |
| tech_stack | ["dotnet","csharp","xunit"] |
| topics | ["unit-testing","integration-testing","mocking","test-data","shouldly","nsubstitute","interface-first"] |
| depends_on | ["abp-framework-patterns"] |
| complements | ["e2e-testing-patterns"] |
| keywords | ["xUnit","Fact","Theory","Shouldly","NSubstitute","TestBase","DataSeeder","InlineData","Interface-First"] |
xUnit Testing Patterns for ABP Framework
Comprehensive testing patterns for ABP Framework applications using xUnit, Shouldly, and NSubstitute.
When to Use
- Writing unit tests for AppServices
- Creating integration tests for ABP modules
- Setting up test data seeders
- Mocking repositories and services
- Testing authorization and validation
- Writing domain service tests
- Interface-first testing (writing tests before implementation)
Test Project Structure
{ProjectName}.TestBase/
├── {ProjectName}TestBase.cs # Base class with common setup
├── {ProjectName}TestBaseModule.cs # Test module configuration
└── {Feature}/
├── {Entity}TestData.cs # Test constants
└── {Entity}TestDataSeedContributor.cs # Test data seeder
{ProjectName}.Application.Tests/
├── {ProjectName}ApplicationTestBase.cs # Application test base
├── {ProjectName}ApplicationTestModule.cs
└── {Feature}/
└── {Entity}AppService_Tests.cs # AppService tests
{ProjectName}.Domain.Tests/
├── {ProjectName}DomainTestBase.cs # Domain test base
├── {ProjectName}DomainTestModule.cs
└── {Feature}/
└── {Entity}Manager_Tests.cs # Domain service tests
Interface-First Testing (NEW)
Write tests against interfaces before implementation exists. This enables parallel development in /add-feature workflow.
Benefits
- Tests can be written as soon as interface contracts exist
- Enables true parallel execution of
abp-developer and qa-engineer
- Tests document expected behavior
- Catches interface design issues early
Example: Testing Against Interface
public class PatientAppService_Tests : ClinicApplicationTestBase
{
private readonly IPatientAppService _patientAppService;
public PatientAppService_Tests()
{
_patientAppService = GetRequiredService<IPatientAppService>();
}
[Fact]
public async Task GetAsync_WithValidId_ReturnsPatient()
{
var patientId = PatientTestData.Patient1Id;
var result = await _patientAppService.GetAsync(patientId);
result.ShouldNotBeNull();
result.Id.ShouldBe(patientId);
result.FirstName.ShouldBe(PatientTestData.Patient1FirstName);
}
}
Core Templates
Test Data Constants
namespace {ProjectName}.{Feature};
public static class PatientTestData
{
public static Guid Patient1Id { get; } = Guid.Parse("00000000-0000-0000-0001-000000000001");
public static Guid Patient2Id { get; } = Guid.Parse("00000000-0000-0000-0001-000000000002");
public static Guid NonExistentId { get; } = Guid.Parse("00000000-0000-0000-0001-999999999999");
public const string Patient1FirstName = "John";
public const string Patient1LastName = "Doe";
public const string Patient1Email = "john.doe@example.com";
public const string Patient2FirstName = "Jane";
public const string Patient2LastName = "Smith";
public const string Patient2Email = "jane.smith@example.com";
public const string ValidFirstName = "New";
public const string ValidLastName = "Patient";
public const string ValidEmail = "new.patient@example.com";
public const string EmptyString = "";
public const string WhitespaceString = " ";
public static readonly string TooLongName = new('X', 256);
public const string InvalidEmail = "not-an-email";
}
Test Data Seeder
using System;
using System.Threading.Tasks;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace {ProjectName}.{Feature};
public class PatientTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
private readonly IRepository<Patient, Guid> _repository;
public PatientTestDataSeedContributor(IRepository<Patient, Guid> repository)
{
_repository = repository;
}
public async Task SeedAsync(DataSeedContext context)
{
if (await _repository.GetCountAsync() > 0)
{
return;
}
await _repository.InsertAsync(
new Patient(
PatientTestData.Patient1Id,
PatientTestData.Patient1FirstName,
PatientTestData.Patient1LastName,
PatientTestData.Patient1Email),
autoSave: true);
await _repository.InsertAsync(
new Patient(
PatientTestData.Patient2Id,
PatientTestData.Patient2FirstName,
PatientTestData.Patient2LastName,
PatientTestData.Patient2Email),
autoSave: true);
}
}
AppService Test Class
For full test class template with all CRUD operations, lifecycle tests, and mocking patterns:
See references/appservice-test-template.md
Quick example:
[Trait("Category", "Integration")]
public class {Entity}AppService_Tests : {ProjectName}ApplicationTestBase
{
private readonly I{Entity}AppService _{entity}AppService;
public {Entity}AppService_Tests()
{
_{entity}AppService = GetRequiredService<I{Entity}AppService>();
}
[Fact]
public async Task GetAsync_WithValidId_Returns{Entity}()
{
var result = await _{entity}AppService.GetAsync({Entity}TestData.{Entity}1Id);
result.ShouldNotBeNull();
result.Id.ShouldBe({Entity}TestData.{Entity}1Id);
}
[Fact]
public async Task CreateAsync_WithValidInput_CreatesAndReturns{Entity}()
{
var input = new Create{Entity}Dto { Name = {Entity}TestData.ValidName };
var result = await _{entity}AppService.CreateAsync(input);
result.ShouldNotBeNull();
result.Id.ShouldNotBe(Guid.Empty);
}
}
Test Categories
1. Happy Path Tests
Test normal successful operations.
[Fact]
public async Task Should_Create_Entity_Successfully()
{
}
2. Validation Tests
Test input validation and constraints.
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData(" ")]
public async Task Should_Reject_Invalid_Name(string? name)
{
var input = new CreateDto { Name = name! };
await Should.ThrowAsync<AbpValidationException>(
() => _service.CreateAsync(input));
}
3. Authorization Tests
Test permission enforcement.
[Fact]
public async Task Should_Require_Permission_To_Create()
{
await WithUnitOfWorkAsync(async () =>
{
await Should.ThrowAsync<AbpAuthorizationException>(
() => _service.CreateAsync(input));
});
}
4. Edge Case Tests
Test boundary conditions and edge cases.
[Fact]
public async Task Should_Handle_Empty_List()
{
var result = await _service.GetListAsync(new GetListInput());
result.TotalCount.ShouldBe(0);
result.Items.ShouldBeEmpty();
}
[Fact]
public async Task Should_Handle_Max_Page_Size()
{
var result = await _service.GetListAsync(
new GetListInput { MaxResultCount = 1000 });
result.Items.Count.ShouldBeLessThanOrEqualTo(100);
}
5. Lifecycle Tests (for Activate/Deactivate patterns)
See references/appservice-test-template.md for full lifecycle test examples.
Test Traits for Organization
[Trait("Category", "Unit")]
[Trait("Feature", "Patients")]
public class PatientAppService_UnitTests { }
[Trait("Category", "Integration")]
[Trait("Feature", "Patients")]
public class PatientAppService_IntegrationTests { }
Mocking with NSubstitute
using NSubstitute;
var repository = Substitute.For<IRepository<{Entity}, Guid>>();
repository.GetAsync(entityId).Returns(entity);
await repository.Received(1).GetAsync(entityId);
For full mocking examples, see references/appservice-test-template.md.
Shouldly Assertion Patterns
result.ShouldNotBeNull();
result.ShouldBeNull();
result.Id.ShouldBe(expectedId);
result.Name.ShouldNotBe(oldName);
result.Items.ShouldNotBeEmpty();
result.Items.ShouldContain(x => x.Name == "Test");
result.Items.Count.ShouldBe(5);
result.Items.ShouldAllBe(x => x.IsActive);
result.TotalCount.ShouldBeGreaterThan(0);
result.TotalCount.ShouldBeLessThanOrEqualTo(100);
result.TotalCount.ShouldBeInRange(1, 100);
result.Name.ShouldStartWith("Test");
result.Email.ShouldContain("@");
result.Name.ShouldNotBeNullOrWhiteSpace();
result.IsActive.ShouldBeTrue();
result.IsDeleted.ShouldBeFalse();
await Should.ThrowAsync<EntityNotFoundException>(
async () => await _service.GetAsync(invalidId));
var ex = await Should.ThrowAsync<BusinessException>(
async () => await _service.CreateAsync(input));
ex.Code.ShouldBe("DuplicateEmail");
Parallel Test Safety
When tests run in parallel, ensure data isolation:
public static class PatientTestData
{
private const string FeaturePrefix = "00000000-0000-0001";
public static Guid Patient1Id { get; } = Guid.Parse($"{FeaturePrefix}-0001-000000000001");
public static Guid Patient2Id { get; } = Guid.Parse($"{FeaturePrefix}-0001-000000000002");
}
Test Checklist
For each AppService, verify:
Shared Knowledge
For foundational patterns, see the shared knowledge base:
References