🎯

api-integration-testing

🎯Skill

from thapaliyabikendra/ai-artifacts

VibeIndex|
What it does

Performs end-to-end integration testing of ABP Framework APIs using xUnit and WebApplicationFactory, covering HTTP requests, authorization, and database interactions.

api-integration-testing

Installation

Install skill:
npx skills add https://github.com/thapaliyabikendra/ai-artifacts --skill api-integration-testing
1
AddedJan 27, 2026

Skill Details

SKILL.md

"Integration testing patterns for ABP Framework APIs using xUnit and WebApplicationFactory. Use when: (1) testing API endpoints end-to-end, (2) verifying HTTP status codes and responses, (3) testing authorization, (4) database integration tests."

Overview

# API Integration Testing

Test ABP Framework APIs end-to-end using xUnit and WebApplicationFactory.

When to Use

  • Testing API endpoints with real HTTP requests
  • Verifying authorization and authentication
  • Testing request/response serialization
  • End-to-end flow validation
  • Database integration testing

Test Project Setup

Project Structure

```

test/

β”œβ”€β”€ [Module].HttpApi.Tests/

β”‚ β”œβ”€β”€ [Module]HttpApiTestBase.cs

β”‚ β”œβ”€β”€ [Module]HttpApiTestModule.cs

β”‚ β”œβ”€β”€ Controllers/

β”‚ β”‚ β”œβ”€β”€ PatientControllerTests.cs

β”‚ β”‚ └── DoctorControllerTests.cs

β”‚ └── TestData/

β”‚ └── TestDataSeeder.cs

```

Test Base Class

```csharp

public abstract class ClinicHttpApiTestBase : AbpIntegratedTest

{

protected HttpClient Client { get; }

protected IServiceProvider Services => ServiceProvider;

protected ClinicHttpApiTestBase()

{

Client = GetHttpClient();

}

protected HttpClient GetHttpClient()

{

var factory = new WebApplicationFactory()

.WithWebHostBuilder(builder =>

{

builder.ConfigureServices(services =>

{

// Replace database with in-memory

services.RemoveAll>();

services.AddDbContext(options =>

options.UseInMemoryDatabase("TestDb"));

});

});

return factory.CreateClient();

}

protected async Task AuthenticateAsAsync(string username, string[] permissions = null)

{

// Set authentication headers

var token = GenerateTestToken(username, permissions);

Client.DefaultRequestHeaders.Authorization =

new AuthenticationHeaderValue("Bearer", token);

}

protected async Task GetAsync(string url)

{

var response = await Client.GetAsync(url);

response.EnsureSuccessStatusCode();

return await response.Content.ReadFromJsonAsync();

}

protected async Task PostAsync(string url, T content)

{

return await Client.PostAsJsonAsync(url, content);

}

}

```

Test Module Configuration

```csharp

[DependsOn(

typeof(ClinicHttpApiModule),

typeof(AbpAspNetCoreTestBaseModule)

)]

public class ClinicHttpApiTestModule : AbpModule

{

public override void ConfigureServices(ServiceConfigurationContext context)

{

// Configure test-specific services

context.Services.AddSingleton();

}

}

```

Common Test Patterns

CRUD Endpoint Tests

```csharp

public class PatientControllerTests : ClinicHttpApiTestBase

{

private const string BaseUrl = "/api/app/patients";

#region GetList

[Fact]

public async Task GetList_ReturnsPagedResult()

{

// Act

var response = await Client.GetAsync(BaseUrl);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.OK);

var result = await response.Content

.ReadFromJsonAsync>();

result.ShouldNotBeNull();

result.Items.ShouldNotBeNull();

}

[Fact]

public async Task GetList_WithFilter_ReturnsFilteredResults()

{

// Act

var response = await Client.GetAsync($"{BaseUrl}?filter=John");

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.OK);

var result = await response.Content

.ReadFromJsonAsync>();

result.Items.ShouldAllBe(p => p.Name.Contains("John"));

}

[Fact]

public async Task GetList_WithPagination_RespectsLimits()

{

// Act

var response = await Client.GetAsync($"{BaseUrl}?skipCount=0&maxResultCount=5");

// Assert

var result = await response.Content

.ReadFromJsonAsync>();

result.Items.Count.ShouldBeLessThanOrEqualTo(5);

}

#endregion

#region Get

[Fact]

public async Task Get_ExistingId_ReturnsPatient()

{

// Arrange

var patientId = TestData.PatientId;

// Act

var response = await Client.GetAsync($"{BaseUrl}/{patientId}");

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.OK);

var patient = await response.Content.ReadFromJsonAsync();

patient.Id.ShouldBe(patientId);

}

[Fact]

public async Task Get_NonExistingId_Returns404()

{

// Act

var response = await Client.GetAsync($"{BaseUrl}/{Guid.NewGuid()}");

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.NotFound);

}

#endregion

#region Create

[Fact]

public async Task Create_ValidInput_Returns201WithEntity()

{

// Arrange

var input = new CreatePatientDto

{

Name = "Jane Doe",

Email = "jane@example.com",

DateOfBirth = new DateTime(1990, 1, 1)

};

// Act

var response = await Client.PostAsJsonAsync(BaseUrl, input);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.Created);

var created = await response.Content.ReadFromJsonAsync();

created.Name.ShouldBe(input.Name);

created.Id.ShouldNotBe(Guid.Empty);

// Verify Location header

response.Headers.Location.ShouldNotBeNull();

}

[Fact]

public async Task Create_MissingRequiredField_Returns400()

{

// Arrange

var input = new CreatePatientDto

{

// Name is missing (required)

Email = "jane@example.com"

};

// Act

var response = await Client.PostAsJsonAsync(BaseUrl, input);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);

var error = await response.Content.ReadFromJsonAsync();

error.Error.ValidationErrors

.ShouldContain(e => e.Members.Contains("Name"));

}

[Fact]

public async Task Create_DuplicateEmail_Returns409()

{

// Arrange

var input = new CreatePatientDto

{

Name = "Another Patient",

Email = TestData.ExistingEmail // Already exists

};

// Act

var response = await Client.PostAsJsonAsync(BaseUrl, input);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.Conflict);

}

#endregion

#region Update

[Fact]

public async Task Update_ValidInput_Returns200()

{

// Arrange

var patientId = TestData.PatientId;

var input = new UpdatePatientDto

{

Name = "Updated Name",

Email = "updated@example.com"

};

// Act

var response = await Client.PutAsJsonAsync($"{BaseUrl}/{patientId}", input);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.OK);

var updated = await response.Content.ReadFromJsonAsync();

updated.Name.ShouldBe(input.Name);

}

[Fact]

public async Task Update_NonExisting_Returns404()

{

// Arrange

var input = new UpdatePatientDto { Name = "Test" };

// Act

var response = await Client.PutAsJsonAsync($"{BaseUrl}/{Guid.NewGuid()}", input);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.NotFound);

}

#endregion

#region Delete

[Fact]

public async Task Delete_ExistingId_Returns204()

{

// Arrange

var patientId = TestData.DeletablePatientId;

// Act

var response = await Client.DeleteAsync($"{BaseUrl}/{patientId}");

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.NoContent);

// Verify deleted (soft delete returns 404)

var getResponse = await Client.GetAsync($"{BaseUrl}/{patientId}");

getResponse.StatusCode.ShouldBe(HttpStatusCode.NotFound);

}

#endregion

}

```

Authorization Tests

```csharp

public class PatientAuthorizationTests : ClinicHttpApiTestBase

{

[Fact]

public async Task Create_WithoutPermission_Returns403()

{

// Arrange

await AuthenticateAsAsync("user-without-create-permission");

var input = new CreatePatientDto { Name = "Test" };

// Act

var response = await Client.PostAsJsonAsync("/api/app/patients", input);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.Forbidden);

}

[Fact]

public async Task Create_WithPermission_Returns201()

{

// Arrange

await AuthenticateAsAsync("admin", new[] { "Clinic.Patients.Create" });

var input = new CreatePatientDto

{

Name = "Test",

Email = "unique@test.com"

};

// Act

var response = await Client.PostAsJsonAsync("/api/app/patients", input);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.Created);

}

[Fact]

public async Task GetList_Unauthenticated_Returns401()

{

// Arrange - clear any auth headers

Client.DefaultRequestHeaders.Authorization = null;

// Act

var response = await Client.GetAsync("/api/app/patients");

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.Unauthorized);

}

[Theory]

[InlineData("Clinic.Patients.Read", HttpStatusCode.OK)]

[InlineData("Clinic.Doctors.Read", HttpStatusCode.Forbidden)]

public async Task GetList_PermissionVariants_ReturnsExpectedStatus(

string permission,

HttpStatusCode expected)

{

// Arrange

await AuthenticateAsAsync("user", new[] { permission });

// Act

var response = await Client.GetAsync("/api/app/patients");

// Assert

response.StatusCode.ShouldBe(expected);

}

}

```

Response Format Tests

```csharp

public class ApiResponseTests : ClinicHttpApiTestBase

{

[Fact]

public async Task ValidationError_HasCorrectFormat()

{

// Arrange

var input = new CreatePatientDto(); // All required fields missing

// Act

var response = await Client.PostAsJsonAsync("/api/app/patients", input);

var content = await response.Content.ReadAsStringAsync();

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);

var error = JsonSerializer.Deserialize(content);

error.Error.ShouldNotBeNull();

error.Error.Code.ShouldBe("Volo.Abp.Validation:ValidationError");

error.Error.ValidationErrors.ShouldNotBeEmpty();

}

[Fact]

public async Task NotFound_HasCorrectFormat()

{

// Act

var response = await Client.GetAsync($"/api/app/patients/{Guid.NewGuid()}");

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.NotFound);

var error = await response.Content

.ReadFromJsonAsync();

error.Error.Code.ShouldContain("EntityNotFound");

}

[Fact]

public async Task PagedResult_HasCorrectStructure()

{

// Act

var response = await Client.GetAsync("/api/app/patients");

var content = await response.Content.ReadAsStringAsync();

// Assert

using var doc = JsonDocument.Parse(content);

doc.RootElement.TryGetProperty("totalCount", out _).ShouldBeTrue();

doc.RootElement.TryGetProperty("items", out var items).ShouldBeTrue();

items.ValueKind.ShouldBe(JsonValueKind.Array);

}

}

```

File Upload Tests

```csharp

public class FileUploadTests : ClinicHttpApiTestBase

{

[Fact]

public async Task UploadProfileImage_ValidFile_Returns200()

{

// Arrange

var patientId = TestData.PatientId;

var content = new MultipartFormDataContent();

var fileContent = new ByteArrayContent(TestData.SampleImageBytes);

fileContent.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg");

content.Add(fileContent, "file", "profile.jpg");

// Act

var response = await Client.PostAsync(

$"/api/app/patients/{patientId}/profile-image",

content);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.OK);

}

[Fact]

public async Task UploadProfileImage_OversizedFile_Returns400()

{

// Arrange

var patientId = TestData.PatientId;

var content = new MultipartFormDataContent();

var largeFile = new byte[10 1024 1024]; // 10MB

content.Add(new ByteArrayContent(largeFile), "file", "large.jpg");

// Act

var response = await Client.PostAsync(

$"/api/app/patients/{patientId}/profile-image",

content);

// Assert

response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);

}

}

```

Test Data Management

```csharp

public static class TestData

{

public static readonly Guid PatientId = Guid.Parse("...");

public static readonly Guid DeletablePatientId = Guid.Parse("...");

public static readonly string ExistingEmail = "existing@example.com";

public static readonly byte[] SampleImageBytes = Convert.FromBase64String("...");

}

public class TestDataSeeder : IDataSeedContributor

{

public async Task SeedAsync(DataSeedContext context)

{

var patientRepository = context.ServiceProvider

.GetRequiredService();

await patientRepository.InsertAsync(new Patient(

TestData.PatientId,

"Test Patient",

TestData.ExistingEmail,

new DateTime(1990, 1, 1)

));

await patientRepository.InsertAsync(new Patient(

TestData.DeletablePatientId,

"Deletable Patient",

"deletable@example.com",

new DateTime(1990, 1, 1)

));

}

}

```

Quick Reference

| Test Type | HTTP Code | Pattern |

|-----------|-----------|---------|

| Success (GET) | 200 | response.StatusCode.ShouldBe(HttpStatusCode.OK) |

| Created | 201 | Verify Location header + body |

| No Content | 204 | For successful DELETE |

| Bad Request | 400 | Check ValidationErrors |

| Unauthorized | 401 | Missing/invalid token |

| Forbidden | 403 | Missing permission |

| Not Found | 404 | Invalid ID |

| Conflict | 409 | Duplicate/business rule violation |

Related Skills

  • xunit-testing-patterns - Base testing patterns
  • test-data-generation - Test data setup
  • abp-framework-patterns - ABP application patterns

More from this repository10

🎯
clean-code-dotnet🎯Skill

Provides clean code guidelines and refactoring techniques for C#/.NET, focusing on improving code readability, maintainability, and adherence to SOLID principles.

🎯
efcore-patterns🎯Skill

Configures and optimizes Entity Framework Core patterns for ABP Framework, focusing on entity configuration, migrations, and relationship design with PostgreSQL.

🎯
abp-api-implementation🎯Skill

Implements comprehensive REST APIs in ABP Framework with robust AppServices, DTOs, pagination, filtering, and authorization for .NET applications.

🎯
abp-entity-patterns🎯Skill

Implements domain layer patterns for ABP Framework, providing robust entity, aggregate, repository, and domain service implementations following DDD principles.

🎯
fluentvalidation-patterns🎯Skill

Validates input DTOs in ABP Framework using FluentValidation with async checks, conditional rules, custom validators, and localized error messages.

🎯
abp-infrastructure-patterns🎯Skill

abp-infrastructure-patterns skill from thapaliyabikendra/ai-artifacts

🎯
content-retrieval🎯Skill

content-retrieval skill from thapaliyabikendra/ai-artifacts

🎯
system-design-patterns🎯Skill

Designs scalable, reliable distributed systems by applying proven architectural patterns and evaluating trade-offs across performance, consistency, and availability.

🎯
abp-contract-scaffolding🎯Skill

Generates ABP Application.Contracts layer scaffolding, enabling parallel development by creating standardized interfaces, DTOs, and permissions for .NET microservices.

🎯
openiddict-authorization🎯Skill

Implements permission-based OAuth 2.0 authorization for ABP Framework using OpenIddict, enabling fine-grained access control and multi-tenant security.