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.

thapaliyabikendra/ai-artifacts10 installsApache-2.0Synced Aug 22

Works with

Claude CodeCursorCodex CLIGitHub CopilotGemini CLI
---
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

← All Testing skills

Check your AI visibility

One URL in, a 0–100 score and the exact fixes out.

RUN THE CHECK

Browse all the tools

15 tools across six categories
13 of them never send your data anywhere

Free · No signup · No trial clock

SEE THE DIRECTORY