Loading...
Loading...
Generate integration tests for ASP.NET Core ABP Framework application services and HTTP APIs. Use when the user requests integration tests, end-to-end tests, API tests, or wants to verify ABP framework integration points (repositories, authorization, validation, multi-tenancy, unit-of-work, data filters). Trigger even if the user just says "add tests" for an ApplicationService — ask if they want unit or integration tests.
npx skill4agent add thapaliyabikendra/ai-artifacts abp-integration-testing| Does the test need to prove… | Test type | Example |
|---|---|---|
| Business logic with all dependencies mocked | Unit test | "CreateAsync validates input and calls repository" |
| EF Core query translation, includes, filters | Integration | "GetListAsync filters soft-deleted entities via ABP data filter" |
| ABP authorization actually blocks calls | Integration | "CreateAsync throws AbpAuthorizationException when permission denied" |
| Real repository persistence + UnitOfWork commit | Integration | "DeleteAsync removes entity from database" |
| Multi-tenant data isolation via TenantId filter | Integration | "GetListAsync only returns current tenant's entities" |
| ABP validation pipeline runs FluentValidation rules | Integration | "CreateAsync throws AbpValidationException for invalid DTO" |
| Object mapping via AutoMapper profiles | Integration | "UpdateAsync maps DTO to entity correctly" |
| HTTP route, model binding, [ApiController] filters | HTTP integration | "POST /api/consumers returns 201 Created" |
| HTTP auth middleware + JWT validation | HTTP integration | "GET /api/consumers returns 401 without token" |
Is the service already covered by unit tests?
├─ Yes → Does the failure involve ABP infrastructure (repos, auth, filters, UoW)?
│ ├─ Yes → Write integration test
│ └─ No → Skip, unit tests are sufficient
└─ No → Start with unit tests first, then add integration tests for framework concernsYourAppServiceApplicationTestBase<YourTestModule>test/YourProject.Application.Tests/YourModule/YourAppServiceTests.csWebApplicationFactory<TStartup>POST /api/consumersConsumerAppService.CreateAsynctest/YourProject.HttpApi.Tests/YourModule/YourControllerTests.csPOST /api/consumers[Authorize]CreateAsync_Should_Throw_AbpAuthorizationException_When_Permission_DeniedPOST_Consumers_Should_Return_401_When_No_JWT_TokenYourAppService.csApplicationTestBaseYourTestModuleWebApplicationFactoryAbpWebApplicationFactoryIntegratedTest| # | Scenario | Why integration matters |
|---|---|---|
| 1 | Happy path with persistence | Proves entity is actually written to DB and queryable |
| 2 | Authorization — denied | Proves ABP's permission system blocks unauthorized calls |
| 3 | Validation failure | Proves ABP's validation pipeline runs (FluentValidation or DataAnnotations) |
| 4 | Data filter isolation | Proves soft-delete or multi-tenant filters work |
| 5 | Transaction rollback | Proves UoW rolls back on exception |
| 6 | Not-found handling | Proves EntityNotFoundException is thrown and handled correctly |
| 7 | Side effects | Proves events are published, domain services are called |
ApplicationTestBase// Look for this pattern in test/YourProject.Application.Tests/
public abstract class YourProjectApplicationTestBase
: YourProjectTestBase<YourProjectApplicationTestModule>
{
// Shared setup already done
}public class ConsumerAppServiceTests : YourProjectApplicationTestBase
{
private readonly IConsumerAppService _sut;
public ConsumerAppServiceTests()
{
_sut = GetRequiredService<IConsumerAppService>();
}
}[DependsOn(
typeof(YourProjectApplicationModule), // The module you're testing
typeof(AbpTestBaseModule), // ABP test foundation
typeof(AbpAuthorizationModule) // If testing authorization
)]
public class YourProjectApplicationTestModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Replace unstable dependencies
context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>());
context.Services.Replace(ServiceDescriptor.Singleton<ISmsSender, NullSmsSender>());
}
}using System;
using System.Threading.Tasks;
using Shouldly;
using Volo.Abp;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Uow;
using Volo.Abp.Validation;
using Xunit;
// Project-specific namespaces[Fact]
public async Task CreateAsync_Should_Persist_Entity_To_Database()
{
// Arrange
var input = new CreateConsumerDto
{
Name = "Test Consumer",
Email = "test@example.com",
ConsumerIdentifier = "test-id-001"
};
// Act
BaseResponseDto response;
await WithUnitOfWorkAsync(async () =>
{
response = await _sut.CreateAsync(input);
});
// Assert
response.Success.ShouldBeTrue();
// Verify entity was actually persisted
await WithUnitOfWorkAsync(async () =>
{
var repo = GetRequiredService<IRepository<Consumer, Guid>>();
var entity = await repo.FirstOrDefaultAsync(c => c.Email == input.Email);
entity.ShouldNotBeNull();
entity.Name.ShouldBe(input.Name);
entity.ConsumerIdentifier.ShouldBe(input.ConsumerIdentifier);
});
}WithUnitOfWorkAsync[Fact]
public async Task CreateAsync_Should_Throw_AbpAuthorizationException_When_User_Lacks_Permission()
{
// Arrange
var input = new CreateConsumerDto { Name = "Blocked", Email = "blocked@test.com" };
// Act & Assert
await Should.ThrowAsync<AbpAuthorizationException>(async () =>
{
await WithUnitOfWorkAsync(async () =>
{
// Current test user does NOT have Consumers.Create permission
await _sut.CreateAsync(input);
});
});
// Verify no entity was created
await WithUnitOfWorkAsync(async () =>
{
var repo = GetRequiredService<IRepository<Consumer, Guid>>();
var entity = await repo.FirstOrDefaultAsync(c => c.Email == input.Email);
entity.ShouldBeNull();
});
}// In test setup, use ABP's ICurrentUser mock or login helpers
protected override void AfterAddApplication(IServiceCollection services)
{
services.AddAlwaysAllowAuthorization(); // For happy-path tests
// OR
services.AddAlwaysDisallowAuthorization(); // For denied tests
}[Fact]
public async Task CreateAsync_Should_Throw_AbpValidationException_When_Email_Invalid()
{
// Arrange
var input = new CreateConsumerDto
{
Name = "Valid Name",
Email = "not-an-email", // Invalid format
ConsumerIdentifier = "test-id"
};
// Act & Assert
var ex = await Should.ThrowAsync<AbpValidationException>(async () =>
{
await WithUnitOfWorkAsync(async () =>
{
await _sut.CreateAsync(input);
});
});
ex.ValidationErrors.ShouldContain(e => e.MemberNames.Contains("Email"));
}[Fact]
public async Task GetListAsync_Should_Not_Return_Soft_Deleted_Entities()
{
// Arrange — seed one active and one soft-deleted entity
Guid activeId = Guid.Empty;
Guid deletedId = Guid.Empty;
await WithUnitOfWorkAsync(async () =>
{
var repo = GetRequiredService<IRepository<Consumer, Guid>>();
var active = new Consumer { Name = "Active", Email = "active@test.com" };
var deleted = new Consumer { Name = "Deleted", Email = "deleted@test.com", IsDeleted = true };
await repo.InsertAsync(active);
await repo.InsertAsync(deleted);
activeId = active.Id;
deletedId = deleted.Id;
});
// Act
PagedResultDto<ConsumerDto> result = null;
await WithUnitOfWorkAsync(async () =>
{
result = await _sut.GetListAsync(new PagedAndSortedResultRequestDto());
});
// Assert
result.Items.ShouldNotContain(c => c.Id == deletedId);
result.Items.ShouldContain(c => c.Id == activeId);
}[Fact]
public async Task GetListAsync_Should_Only_Return_Current_Tenant_Entities()
{
// Arrange — seed entities for two tenants
Guid tenant1EntityId = Guid.Empty;
Guid tenant2EntityId = Guid.Empty;
await WithUnitOfWorkAsync(async () =>
{
var repo = GetRequiredService<IRepository<Consumer, Guid>>();
var tenant1Entity = new Consumer { Name = "Tenant1", TenantId = TestTenant1Id };
var tenant2Entity = new Consumer { Name = "Tenant2", TenantId = TestTenant2Id };
await repo.InsertAsync(tenant1Entity);
await repo.InsertAsync(tenant2Entity);
tenant1EntityId = tenant1Entity.Id;
tenant2EntityId = tenant2Entity.Id;
});
// Act — query as Tenant1
PagedResultDto<ConsumerDto> result = null;
await WithUnitOfWorkAsync(async () =>
{
using (CurrentTenant.Change(TestTenant1Id))
{
result = await _sut.GetListAsync(new PagedAndSortedResultRequestDto());
}
});
// Assert
result.Items.ShouldContain(c => c.Id == tenant1EntityId);
result.Items.ShouldNotContain(c => c.Id == tenant2EntityId);
}[Fact]
public async Task CreateAsync_Should_Rollback_Transaction_When_Validation_Fails()
{
// Arrange
var validInput = new CreateConsumerDto { Name = "Valid", Email = "valid@test.com" };
var invalidInput = new CreateConsumerDto { Name = "", Email = "invalid" }; // Fails validation
// Act — first call succeeds, second fails
await WithUnitOfWorkAsync(async () =>
{
await _sut.CreateAsync(validInput);
});
await Should.ThrowAsync<AbpValidationException>(async () =>
{
await WithUnitOfWorkAsync(async () =>
{
await _sut.CreateAsync(invalidInput);
});
});
// Assert — only the valid entity persisted
await WithUnitOfWorkAsync(async () =>
{
var repo = GetRequiredService<IRepository<Consumer, Guid>>();
var all = await repo.GetListAsync();
all.Count.ShouldBe(1);
all[0].Email.ShouldBe(validInput.Email);
});
}WebApplicationFactoryAbpWebApplicationFactoryIntegratedTestpublic class ConsumerControllerTests : AbpWebApplicationFactoryIntegratedTest<YourProjectHttpApiTestModule>
{
[Fact]
public async Task POST_Consumers_Should_Return_201_Created_With_Valid_Input()
{
// Arrange
var client = GetRequiredService<HttpClient>();
var input = new CreateConsumerDto { Name = "Test", Email = "test@test.com" };
// Act
var response = await client.PostAsJsonAsync("/api/consumers", input);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.Created);
var result = await response.Content.ReadFromJsonAsync<ConsumerDto>();
result.Name.ShouldBe(input.Name);
}
[Fact]
public async Task POST_Consumers_Should_Return_401_When_Not_Authenticated()
{
// Arrange
var client = CreateAnonymousClient(); // No auth token
var input = new CreateConsumerDto { Name = "Test", Email = "test@test.com" };
// Act
var response = await client.PostAsJsonAsync("/api/consumers", input);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);
}
}WithUnitOfWorkAsyncShouldly[Fact]test/YourProject.Application.Tests/// Arrange// Act// AssertWithUnitOfWorkAsyncFile written: test/YourProject.Application.Tests/Consumers/ConsumerAppServiceTests.cs
Integration tests generated: 6
| # | Test method | Scenario |
|---|---|---|
| 1 | CreateAsync_Should_Persist_Entity_To_Database | Happy path + DB verification |
| 2 | CreateAsync_Should_Throw_AbpAuthorizationException_When_User_Lacks_Permission | Authorization denied |
| 3 | CreateAsync_Should_Throw_AbpValidationException_When_Email_Invalid | Validation failure |
| 4 | GetListAsync_Should_Not_Return_Soft_Deleted_Entities | Soft-delete filter |
| 5 | GetListAsync_Should_Only_Return_Current_Tenant_Entities | Multi-tenant isolation |
| 6 | CreateAsync_Should_Rollback_Transaction_When_Validation_Fails | UoW rollback |
Run with: dotnet test test/YourProject.Application.Tests| Pitfall | Why it fails | Solution |
|---|---|---|
Mocking | Integration test should use real repos | Remove all |
Forgetting | DB changes not committed/visible | Wrap all DB operations in UoW scope |
| Not verifying DB state | Test only checks DTO, not persistence | Add second query to verify entity exists |
| Reusing test data across tests | Tests fail when run in parallel | Seed fresh data per test |
| Testing business logic already covered by unit tests | Wastes time, slows CI | Only test framework integration concerns |
| Using raw SQL for seeding | Bypasses ABP infrastructure | Use repository + UoW for seeding |