| name | abp-testing |
| description | ABP testing patterns - integration tests over unit tests, GetRequiredService, IDataSeedContributor, Shouldly assertions, AddAlwaysAllowAuthorization, NSubstitute mocking, WithUnitOfWorkAsync. Use when writing or reviewing tests for application services, domain services, or repositories in ABP projects. |
ABP Testing Patterns
Docs: https://abp.io/docs/latest/testing
Test Project Structure
| Project | Purpose | Base Class |
|---|
*.Domain.Tests | Domain logic, entities, domain services | *DomainTestBase |
*.Application.Tests | Application services | *ApplicationTestBase |
*.EntityFrameworkCore.Tests | Repository implementations | *EntityFrameworkCoreTestBase |
Integration Test Approach
ABP recommends integration tests over unit tests:
- Tests run with real services and database (SQLite in-memory)
- No mocking of internal services
- Each test gets a fresh database instance
Application Service Test
public class BookAppService_Tests : MyProjectApplicationTestBase
{
private readonly IBookAppService _bookAppService;
public BookAppService_Tests()
{
_bookAppService = GetRequiredService<IBookAppService>();
}
[Fact]
public async Task Should_Get_List_Of_Books()
{
var result = await _bookAppService.GetListAsync(
new PagedAndSortedResultRequestDto()
);
result.TotalCount.ShouldBeGreaterThan(0);
result.Items.ShouldContain(b => b.Name == "Test Book");
}
[Fact]
public async Task Should_Create_Book()
{
var input = new CreateBookDto
{
Name = "New Book",
Price = 19.99m
};
var result = await _bookAppService.CreateAsync(input);
result.Id.ShouldNotBe(Guid.Empty);
result.Name.ShouldBe("New Book");
result.Price.ShouldBe(19.99m);
}
[Fact]
public async Task Should_Not_Create_Book_With_Invalid_Name()
{
var input = new CreateBookDto
{
Name = "",
Price = 10m
};
await Should.ThrowAsync<AbpValidationException>(async () =>
{
await _bookAppService.CreateAsync(input);
});
}
}
Domain Service Test
public class BookManager_Tests : MyProjectDomainTestBase
{
private readonly BookManager _bookManager;
private readonly IBookRepository _bookRepository;
public BookManager_Tests()
{
_bookManager = GetRequiredService<BookManager>();
_bookRepository = GetRequiredService<IBookRepository>();
}
[Fact]
public async Task Should_Create_Book()
{
var book = await _bookManager.CreateAsync("Test Book", 29.99m);
book.ShouldNotBeNull();
book.Name.ShouldBe("Test Book");
book.Price.ShouldBe(29.99m);
}
[Fact]
public async Task Should_Not_Allow_Duplicate_Book_Name()
{
await _bookManager.CreateAsync("Existing Book", 10m);
var exception = await Should.ThrowAsync<BusinessException>(async () =>
{
await _bookManager.CreateAsync("Existing Book", 20m);
});
exception.Code.ShouldBe("MyProject:BookNameAlreadyExists");
}
}
Test Naming Convention
Use descriptive names:
public async Task Should_Create_Book_When_Input_Is_Valid()
public async Task Should_Throw_BusinessException_When_Name_Already_Exists()
public async Task Should_Return_Empty_List_When_No_Books_Exist()
Arrange-Act-Assert (AAA)
[Fact]
public async Task Should_Update_Book_Price()
{
var bookId = await CreateTestBookAsync();
var newPrice = 39.99m;
var result = await _bookAppService.UpdateAsync(bookId, new UpdateBookDto
{
Price = newPrice
});
result.Price.ShouldBe(newPrice);
}
Assertions with Shouldly
ABP uses Shouldly library:
result.ShouldNotBeNull();
result.Name.ShouldBe("Expected Name");
result.Price.ShouldBeGreaterThan(0);
result.Items.ShouldContain(x => x.Id == expectedId);
result.Items.ShouldBeEmpty();
result.Items.Count.ShouldBe(5);
await Should.ThrowAsync<BusinessException>(async () =>
{
await _service.DoSomethingAsync();
});
var ex = await Should.ThrowAsync<BusinessException>(async () =>
{
await _service.DoSomethingAsync();
});
ex.Code.ShouldBe("MyProject:ErrorCode");
Test Data Seeding
public class MyProjectTestDataSeedContributor : IDataSeedContributor, ITransientDependency
{
public static readonly Guid TestBookId = Guid.Parse("...");
private readonly IBookRepository _bookRepository;
private readonly IGuidGenerator _guidGenerator;
public async Task SeedAsync(DataSeedContext context)
{
await _bookRepository.InsertAsync(
new Book(TestBookId, "Test Book", 19.99m, Guid.Empty),
autoSave: true
);
}
}
Disabling Authorization in Tests
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddAlwaysAllowAuthorization();
}
Mocking External Services
Use NSubstitute when needed:
public override void ConfigureServices(ServiceConfigurationContext context)
{
var emailSender = Substitute.For<IEmailSender>();
emailSender.SendAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<string>())
.Returns(Task.CompletedTask);
context.Services.AddSingleton(emailSender);
}
Testing with Specific User
[Fact]
public async Task Should_Get_Current_User_Books()
{
await WithUnitOfWorkAsync(async () =>
{
using (CurrentUser.Change(TestData.UserId))
{
var result = await _bookAppService.GetMyBooksAsync();
result.Items.ShouldAllBe(b => b.CreatorId == TestData.UserId);
}
});
}
Testing Multi-Tenancy
[Fact]
public async Task Should_Filter_Books_By_Tenant()
{
using (CurrentTenant.Change(TestData.TenantId))
{
var result = await _bookAppService.GetListAsync(new GetBookListDto());
}
}
Best Practices
- Each test should be independent
- Don't share state between tests
- Use meaningful test data
- Test edge cases and error conditions
- Keep tests focused on single behavior
- Use test data seeders for common data
- Avoid testing framework internals