xunit-testing-patterns

xUnit Testing Patterns for ABP Framework

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "xunit-testing-patterns" with this command: npx skills add thapaliyabikendra/ai-artifacts/thapaliyabikendra-ai-artifacts-xunit-testing-patterns

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

// This test compiles and is ready to run once implementation exists public class PatientAppService_Tests : ClinicApplicationTestBase { private readonly IPatientAppService _patientAppService;

public PatientAppService_Tests()
{
    // Resolves implementation from DI container
    _patientAppService = GetRequiredService<IPatientAppService>();
}

[Fact]
public async Task GetAsync_WithValidId_ReturnsPatient()
{
    // Arrange - uses test data constants
    var patientId = PatientTestData.Patient1Id;

    // Act - calls interface method
    var result = await _patientAppService.GetAsync(patientId);

    // Assert - validates contract expectations
    result.ShouldNotBeNull();
    result.Id.ShouldBe(patientId);
    result.FirstName.ShouldBe(PatientTestData.Patient1FirstName);
}

}

Core Templates

Test Data Constants

// {ProjectName}.TestBase/{Feature}/{Entity}TestData.cs namespace {ProjectName}.{Feature};

public static class PatientTestData { // Use deterministic GUIDs for test reproducibility 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");

// Valid test data
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";

// Valid data for create tests
public const string ValidFirstName = "New";
public const string ValidLastName = "Patient";
public const string ValidEmail = "new.patient@example.com";

// Invalid data for negative tests
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

// {ProjectName}.TestBase/{Feature}/{Entity}TestDataSeedContributor.cs 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&#x3C;Patient, Guid> repository)
{
    _repository = repository;
}

public async Task SeedAsync(DataSeedContext context)
{
    // Idempotent seeding
    if (await _repository.GetCountAsync() > 0)
    {
        return;
    }

    // Patient 1 - Active
    await _repository.InsertAsync(
        new Patient(
            PatientTestData.Patient1Id,
            PatientTestData.Patient1FirstName,
            PatientTestData.Patient1LastName,
            PatientTestData.Patient1Email),
        autoSave: true);

    // Patient 2 - For deletion/update tests
    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&#x3C;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() { // Standard create with valid data }

  1. 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)); }

  1. Authorization Tests

Test permission enforcement.

[Fact] public async Task Should_Require_Permission_To_Create() { // Login as user without permission await WithUnitOfWorkAsync(async () => { await Should.ThrowAsync<AbpAuthorizationException>( () => _service.CreateAsync(input)); }); }

  1. Edge Case Tests

Test boundary conditions and edge cases.

[Fact] public async Task Should_Handle_Empty_List() { // Clear all data 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); // Capped }

  1. Lifecycle Tests (for Activate/Deactivate patterns)

See references/appservice-test-template.md for full lifecycle test examples.

Test Traits for Organization

// Categorize tests for selective execution [Trait("Category", "Unit")] [Trait("Feature", "Patients")] public class PatientAppService_UnitTests { }

[Trait("Category", "Integration")] [Trait("Feature", "Patients")] public class PatientAppService_IntegrationTests { }

// Run by category: // dotnet test --filter "Category=Unit" // dotnet test --filter "Feature=Patients"

Mocking with NSubstitute

using NSubstitute;

// Create mock var repository = Substitute.For<IRepository<{Entity}, Guid>>();

// Setup return value repository.GetAsync(entityId).Returns(entity);

// Verify call await repository.Received(1).GetAsync(entityId);

For full mocking examples, see references/appservice-test-template.md.

Shouldly Assertion Patterns

// Null checks result.ShouldNotBeNull(); result.ShouldBeNull();

// Equality result.Id.ShouldBe(expectedId); result.Name.ShouldNotBe(oldName);

// Collections result.Items.ShouldNotBeEmpty(); result.Items.ShouldContain(x => x.Name == "Test"); result.Items.Count.ShouldBe(5); result.Items.ShouldAllBe(x => x.IsActive);

// Numeric comparisons result.TotalCount.ShouldBeGreaterThan(0); result.TotalCount.ShouldBeLessThanOrEqualTo(100); result.TotalCount.ShouldBeInRange(1, 100);

// String assertions result.Name.ShouldStartWith("Test"); result.Email.ShouldContain("@"); result.Name.ShouldNotBeNullOrWhiteSpace();

// Boolean assertions result.IsActive.ShouldBeTrue(); result.IsDeleted.ShouldBeFalse();

// Exception assertions 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:

// Use unique IDs per test class public static class PatientTestData { // Include feature identifier in GUIDs to avoid collisions 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:

  • GetAsync - valid ID returns entity

  • GetAsync - non-existent ID throws EntityNotFoundException

  • GetListAsync - returns paginated results

  • GetListAsync - respects filters

  • GetListAsync - respects pagination

  • CreateAsync - valid input creates entity

  • CreateAsync - empty required field throws validation

  • CreateAsync - exceeds max length throws validation

  • UpdateAsync - valid input updates entity

  • UpdateAsync - non-existent ID throws EntityNotFoundException

  • DeleteAsync - valid ID deletes entity

  • DeleteAsync - non-existent ID throws EntityNotFoundException

  • (If applicable) ActivateAsync - activates inactive entity

  • (If applicable) DeactivateAsync - deactivates active entity

Shared Knowledge

For foundational patterns, see the shared knowledge base:

Topic File Description

Folder structure knowledge/conventions/folder-structure.md Test project layout

Naming conventions knowledge/conventions/naming.md Test class naming

CRUD example knowledge/examples/crud-entity.md Test target example

References

  • references/integration-test-patterns.md - Advanced integration testing

  • references/test-fixtures.md - Shared test fixtures

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

abp-infrastructure-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

abp-entity-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

abp-api-implementation

No summary provided by upstream source.

Repository SourceNeeds Review
General

abp-service-patterns

No summary provided by upstream source.

Repository SourceNeeds Review