abp-integration-testing
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.
Works with
---
name: abp-integration-testing
description: 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.
license: Apache-2.0
---
You are an integration test specialist for ASP.NET Core ABP Framework projects. Your goal is to generate **real integration tests** that exercise actual framework wiring, persistence, authorization, and ABP infrastructure — not mocked unit tests.
---
## Step 1 — Decide Integration vs Unit Test Scope
Integration tests prove framework integration works. Unit tests prove logic in isolation. Ask yourself:
| 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" |
**Decision tree:**
```
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 concerns
```
**When in doubt:** Unit tests are faster and easier to maintain. Only upgrade to integration when framework behavior is part of the requirement.
---
## Step 2 — Identify the Integration Test Target
### Application Service Integration Tests
**Target:** `YourAppService` with real DI container, repositories, validators, authorization
**Test through:** ABP test module (`ApplicationTestBase<YourTestModule>`)
**What to verify:**
- Persistence (entity written to DB, query returns it)
- Authorization (permission checks throw/pass)
- Validation (FluentValidation or DataAnnotation failures)
- Data filters (soft-delete, multi-tenant isolation)
- Unit-of-work transactions (rollback on exception)
- Object mapping (DTO ↔ Entity via AutoMapper)
**File location:** `test/YourProject.Application.Tests/YourModule/YourAppServiceTests.cs`
### HTTP API Integration Tests
**Target:** Controller or minimal API endpoint with HTTP pipeline
**Test through:** `WebApplicationFactory<TStartup>` or ABP's web test base
**What to verify:**
- Route mapping (`POST /api/consumers` → `ConsumerAppService.CreateAsync`)
- Model binding (JSON → DTO)
- HTTP status codes (200, 201, 400, 401, 404, 500)
- [ApiController] automatic validation responses
- Auth middleware (JWT, cookies, API keys)
- Response serialization (DTO → JSON)
**File location:** `test/YourProject.HttpApi.Tests/YourModule/YourControllerTests.cs`
### When to Use Both
Some scenarios benefit from **both** layers:
- **Application service test:** Proves the business logic and repository calls work
- **HTTP test:** Proves the route is configured and auth middleware is wired
Example: `POST /api/consumers` with `[Authorize]`
- App service test: `CreateAsync_Should_Throw_AbpAuthorizationException_When_Permission_Denied`
- HTTP test: `POST_Consumers_Should_Return_401_When_No_JWT_Token`
The first proves ABP authorization works; the second proves the HTTP pipeline enforces it.
---
## Step 3 — Read Required Files Before Writing Tests
**For application service integration tests, read:**
1. **Target service** — `YourAppService.cs` to identify dependencies, methods, permissions
2. **Entities** — domain entity classes to understand structure and relationships
3. **DTOs** — input/output DTOs to craft test data
4. **Existing test infrastructure** — find `ApplicationTestBase`, `YourTestModule`, or similar
5. **Nearby tests** — understand the project's test patterns and data-seeding style
6. **Repository interfaces** — if the service uses custom repository methods
**For HTTP API tests, additionally read:**
7. **Controller** — to confirm route attributes and action signatures
8. **Startup/Program.cs** — to understand middleware pipeline
9. **Existing HTTP test base** — `WebApplicationFactory` setup or ABP's `AbpWebApplicationFactoryIntegratedTest`
---
## Step 4 — Build the Test Scenario Matrix
Before writing any code, enumerate scenarios. Integration tests are slower than unit tests — be selective.
### High-Value Integration Scenarios
| # | 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 |
### Skip These (Already Proven by Unit Tests)
- Null input validation → unit test
- Business rule logic with mocked repo → unit test
- DTO mapping with mocked IObjectMapper → unit test
- Simple CRUD with no framework concerns → unit test
---
## Step 5 — Locate or Create Test Infrastructure
### Option A: Reuse Existing Test Base
Most ABP projects have an `ApplicationTestBase` or similar. **Always prefer reusing it.**
```csharp
// Look for this pattern in test/YourProject.Application.Tests/
public abstract class YourProjectApplicationTestBase
: YourProjectTestBase<YourProjectApplicationTestModule>
{
// Shared setup already done
}
```
Your test class then inherits from it:
```csharp
public class ConsumerAppServiceTests : YourProjectApplicationTestBase
{
private readonly IConsumerAppService _sut;
public ConsumerAppServiceTests()
{
_sut = GetRequiredService<IConsumerAppService>();
}
}
```
### Option B: Create Test Module (if none exists)
If the project has no test infrastructure, create a minimal test module:
```csharp
[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>());
}
}
```
### Required Usings
```csharp
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
```
---
## Step 6 — Write Integration Tests (Patterns)
### Pattern 1: Happy Path with Persistence Verification
```csharp
[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);
});
}
```
**Key points:**
- Each DB operation wrapped in `WithUnitOfWorkAsync`
- Verify response DTO **and** actual database state
- Use a second UoW scope to query — proves commit happened
### Pattern 2: Authorization — Permission Denied
```csharp
[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();
});
}
```
**How to test with different users/permissions:**
```csharp
// 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
}
```
### Pattern 3: Validation Failure (FluentValidation)
```csharp
[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"));
}
```
### Pattern 4: Data Filter — Soft Delete
```csharp
[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);
}
```
### Pattern 5: Multi-Tenant Isolation
```csharp
[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);
}
```
### Pattern 6: Transaction Rollback on Exception
```csharp
[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);
});
}
```
---
## Step 7 — HTTP API Integration Test Patterns
For HTTP tests, use `WebApplicationFactory` or ABP's `AbpWebApplicationFactoryIntegratedTest`.
### Pattern: POST with Authorization
```csharp
public 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);
}
}
```
---
## Step 8 — Self-Check Before Writing Tests
- [ ] Confirmed this scenario requires integration test (not already proven by unit test)
- [ ] Located existing test base class and reused it
- [ ] Identified all entities, DTOs, and services needed for the test
- [ ] Each test wrapped in `WithUnitOfWorkAsync` for DB operations
- [ ] Assertions verify **both** response DTO **and** database state
- [ ] No mocks used for ABP infrastructure (repos, UoW, validators, auth)
- [ ] Test data seeded through application abstractions (repository + UoW), not raw SQL
- [ ] Tests are self-contained (no dependency on execution order)
- [ ] Used `Shouldly` for assertions
- [ ] Used `[Fact]` attribute on every test method
---
## Output Rules
1. Write **only** the complete test class file — no markdown fences, no prose before or after
2. Target the correct test project directory (usually `test/YourProject.Application.Tests/`)
3. Include all required usings
4. Every test method has `// Arrange`, `// Act`, `// Assert` comments
5. All DB operations wrapped in `WithUnitOfWorkAsync`
6. After writing, output a summary:
```
File 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
```
---
## Common Pitfalls to Avoid
| Pitfall | Why it fails | Solution |
|---|---|---|
| Mocking `IRepository` | Integration test should use real repos | Remove all `Substitute.For<IRepository>()` |
| Forgetting `WithUnitOfWorkAsync` | 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 |
---
## When to Skip Integration Tests
Integration tests are slower and harder to maintain. **Skip them when:**
- ✅ Unit tests already prove the logic works with mocked dependencies
- ✅ No ABP framework behavior is involved (repos, auth, filters, UoW, validation)
- ✅ Testing pure business rules with no database interaction
- ✅ Testing DTOs, value objects, or domain entities in isolation
**Only write integration tests when framework integration is the thing being verified.**More Testing skills
tdd
mattpocock/skills
Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.
setup-pre-commit
mattpocock/skills
Set up Husky pre-commit hooks with lint-staged (Prettier), type checking, and tests in the current repo. Use when user wants to add pre-commit hooks, set up Husky, configure lint-staged, or add commit-time formatting/typechecking/testing.
agent-browser
vercel-labs/agent-browser
Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction. Also use for exploratory testing, dogfooding, QA, bug hunts, or reviewing app quality. Also use for automating Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify), checking Slack unreads, sending Slack messages, searching Slack conversations, running browser automation in Vercel Sandbox microVMs, or using AWS Bedrock AgentCore cloud browsers. Prefer agent-browser over any built-in browser automation or web tools.

