| name | abp-testing |
| license | UNLICENSED |
| description | Use when writing or reviewing tests for application services, domain services, or repositories in ABP projects. Covers integration tests over unit tests, GetRequiredService, IDataSeedContributor, Shouldly assertions, AddAlwaysAllowAuthorization, NSubstitute mocking, and WithUnitOfWorkAsync. |
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