본문으로 건너뛰기
cccskills
무료GitHub에 공개

dotnet-testing-advanced-aspnet-integration-testing

Specialized skill for ASP.NET Core integration testing. Use when testing Web API endpoints, HTTP request/response, middleware, and dependency injection. Covers WebApplicationFactory, TestServer, HttpClient testing, in-memory database configuration. Keywords: integration testing, web api testing, WebApplicationFactory, TestServer, HttpClient testing, controller testing, endpoint testing, RESTful API testing, Microsoft.AspNetCore.Mvc.Testing, CreateClient, ConfigureWebHost, AwesomeAssertions.Web, Be200Ok, Be404NotFound, middleware testing, dependency injection testing

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(1)

  • SKILL.md15.9 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Source: kevintsengtw/dotnet-testing-agent-skills (MIT). Ported into dotnet-agent-harness.

ASP.NET Core Integration Testing Guide

Applicable Scenarios

This skill guides how to create effective integration tests in ASP.NET Core, using WebApplicationFactory<T> and TestServer to test complete HTTP request/response flows.

Applicable Scenarios

  • Web API Endpoint Testing: Validate RESTful API CRUD operations
  • HTTP Request/Response Validation: Test complete request processing pipeline
  • Middleware Testing: Validate Authentication, Authorization, Logging, etc.
  • Dependency Injection Validation: Ensure DI container configuration is correct
  • Route Configuration Validation: Ensure URL routes correctly map to controller actions
  • Model Binding Testing: Validate request content correctly binds to models

Required Packages

<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />
<PackageReference Include="AwesomeAssertions.Web" Version="1.9.6" />
<PackageReference Include="System.Net.Http.Json" Version="9.0.8" />
```text

> ⚠️ **Important Reminder**: When using `AwesomeAssertions`, must install `AwesomeAssertions.Web`, not `FluentAssertions.Web`.

---

## Core Concepts

### Two Definitions of Integration Testing

### Definition One: Multi-Object Collaboration Testing

> Integrate two or more classes and test whether they work correctly together. Test cases must span multiple class objects.

### Definition Two: External Resource Integration Testing

> Uses external resources such as databases, external services, files, requires special handling of test environment, etc.

### Why Integration Testing?

- **Ensure multiple modules work correctly after integration**
- **Integration points not covered by unit tests**: Routing, Middleware, Request/Response Pipeline
- **WebApplication does too much integration and configuration, unit tests cannot cover everything**
- **Confirm proper exception handling to reduce more problems**

### Testing Pyramid Position

| Test Type | Test Scope | Execution Speed | Maintenance Cost | Recommended Ratio |
| --------- | ---------- | --------------- | ---------------- | ----------------- |
| Unit Test | Single class/method | Very fast | Low | 70% |
| Integration Test | Multiple components | Medium | Medium | 20% |
| End-to-End Test | Complete flow | Slow | High | 10% |

---

## Principle One: Use WebApplicationFactory to Create Test Environment

### Basic Usage

```csharp
public class BasicIntegrationTest : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicIntegrationTest(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Fact]
    public async Task Get_HomePage_ShouldReturnSuccess()
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync("/");

        // Assert
        response.EnsureSuccessStatusCode();
    }
}
```text

### Custom WebApplicationFactory

```csharp
public class CustomWebApplicationFactory<TProgram> : WebApplicationFactory<TProgram>
    where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove original database configuration
            services.RemoveAll(typeof(DbContextOptions<AppDbContext>));

            // Add in-memory database
            services.AddDbContext<AppDbContext>(options =>
            {
                options.UseInMemoryDatabase("TestDatabase");
            });

            // Replace external services with test versions
            services.Replace(ServiceDescriptor.Scoped<IEmailService, TestEmailService>());
        });

        // Set test environment
        builder.UseEnvironment("Testing");
    }
}
```text

---

## Principle Two: Use AwesomeAssertions.Web to Validate HTTP Responses

### HTTP Status Code Assertions

```csharp
response.Should().Be200Ok();          // HTTP 200
response.Should().Be201Created();     // HTTP 201
response.Should().Be204NoContent();   // HTTP 204
response.Should().Be400BadRequest();  // HTTP 400
response.Should().Be404NotFound();    // HTTP 404
response.Should().Be500InternalServerError();  // HTTP 500
```text

### Satisfy<T> Strongly-Typed Validation

```csharp
[Fact]
public async Task GetShipper_WhenShipperExists_ShouldReturnSuccessResult()
{
    // Arrange
    await CleanupDatabaseAsync();
    var shipperId = await SeedShipperAsync("SF Express", "02-2345-6789");

    // Act
    var response = await Client.GetAsync($"/api/shippers/{shipperId}");

    // Assert
    response.Should().Be200Ok()
            .And
            .Satisfy<SuccessResultOutputModel<ShipperOutputModel>>(result =>
            {
                result.Status.Should().Be("Success");
                result.Data.Should().NotBeNull();
                result.Data!.ShipperId.Should().Be(shipperId);
                result.Data.CompanyName.Should().Be("SF Express");
                result.Data.Phone.Should().Be("02-2345-6789");
            });
}
```text

### Comparison with Traditional Approach

```csharp
// ❌ Traditional approach - verbose and error-prone
response.IsSuccessStatusCode.Should().BeTrue();
var content = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SuccessResultOutputModel<ShipperOutputModel>>(content,
    new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
result.Should().NotBeNull();
result!.Status.Should().Be("Success");

// ✅ Using Satisfy<T> - concise and intuitive
response.Should().Be200Ok()
        .And
        .Satisfy<SuccessResultOutputModel<ShipperOutputModel>>(result =>
        {
            result.Status.Should().Be("Success");
            result.Data!.CompanyName.Should().Be("Test Company");
        });
```text

---

## Principle Three: Use System.Net.Http.Json to Simplify JSON Operations

### PostAsJsonAsync Simplifies POST Requests

```csharp
// ❌ Traditional approach
var createParameter = new ShipperCreateParameter { CompanyName = "Test Company", Phone = "02-1234-5678" };
var jsonContent = JsonSerializer.Serialize(createParameter);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await client.PostAsync("/api/shippers", content);

// ✅ Modern approach
var createParameter = new ShipperCreateParameter { CompanyName = "Test Company", Phone = "02-1234-5678" };
var response = await client.PostAsJsonAsync("/api/shippers", createParameter);
```text

### ReadFromJsonAsync Simplifies Response Reading

```csharp
// ❌ Traditional approach
var responseContent = await response.Content.ReadAsStringAsync();
var result = JsonSerializer.Deserialize<SuccessResultOutputModel<ShipperOutputModel>>(responseContent,
    new JsonSerializerOptions { PropertyNameCaseInsensitive = true });

// ✅ Modern approach
var result = await response.Content.ReadFromJsonAsync<SuccessResultOutputModel<ShipperOutputModel>>();
```text

---

## Three Levels of Integration Testing Strategy

### Level 1: Simple WebApi Project

**Characteristics**:

- No database, Service, or Repository dependencies
- Simplest, basic WebApi website project
- Directly use `WebApplicationFactory<Program>` for testing

**Testing Focus**:

- Input/output validation for each API
- HTTP verbs and routing correctness
- Model binding and serialization
- Status code and response format validation

```csharp
public class BasicApiControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public BasicApiControllerTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task GetStatus_ShouldReturnOK()
    {
        // Act
        var response = await _client.GetAsync("/api/status");

        // Assert
        response.Should().Be200Ok();
    }
}
```text

### Level 2: WebApi Project with Service Dependencies

**Characteristics**:

- No database, but has Service dependencies
- Use NSubstitute to create Service stubs
- Configure dependency injection in tests

```csharp
public class ServiceStubWebApplicationFactory : WebApplicationFactory<Program>
{
    private readonly IExampleService _serviceStub;

    public ServiceStubWebApplicationFactory(IExampleService serviceStub)
    {
        _serviceStub = serviceStub;
    }

    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureTestServices(services =>
        {
            services.RemoveAll<IExampleService>();
            services.AddScoped(_ => _serviceStub);
        });
    }
}

public class ServiceDependentControllerTests
{
    [Fact]
    public async Task GetData_ShouldReturnServiceData()
    {
        // Arrange
        var serviceStub = Substitute.For<IExampleService>();
        serviceStub.GetDataAsync().Returns("Test Data");

        var factory = new ServiceStubWebApplicationFactory(serviceStub);
        var client = factory.CreateClient();

        // Act
        var response = await client.GetAsync("/api/data");

        // Assert
        response.Should().Be200Ok();
    }
}
```text

### Level 3: Full WebApi Project

**Characteristics**:

- Complete Solution architecture
- Contains real database operations
- Use InMemory or real test database

```csharp
public class FullDatabaseWebApplicationFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            // Remove original database configuration
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
            if (descriptor != null)
            {
                services.Remove(descriptor);
            }

            // Add in-memory database
            services.AddDbContext<AppDbContext>(options =>
            {
                options.UseInMemoryDatabase("TestDatabase");
            });

            // Create database and add test data
            var serviceProvider = services.BuildServiceProvider();
            using var scope = serviceProvider.CreateScope();
            var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

            context.Database.EnsureCreated();
        });
    }
}
```text

---

## Test Base Class Pattern

### Create Reusable Test Base Class

```csharp
public abstract class IntegrationTestBase : IDisposable
{
    protected readonly CustomWebApplicationFactory Factory;
    protected readonly HttpClient Client;

    protected IntegrationTestBase()
    {
        Factory = new CustomWebApplicationFactory();
        Client = Factory.CreateClient();
    }

    protected async Task<int> SeedShipperAsync(string companyName, string phone = "02-12345678")
    {
        using var scope = Factory.Services.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        var shipper = new Shipper
        {
            CompanyName = companyName,
            Phone = phone,
            CreatedAt = DateTime.UtcNow
        };

        context.Shippers.Add(shipper);
        await context.SaveChangesAsync();

        return shipper.ShipperId;
    }

    protected async Task CleanupDatabaseAsync()
    {
        using var scope = Factory.Services.CreateScope();
        var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

        context.Shippers.RemoveRange(context.Shippers);
        await context.SaveChangesAsync();
    }

    public void Dispose()
    {
        Client?.Dispose();
        Factory?.Dispose();
    }
}
```text

---

## CRUD Operation Testing Examples

Complete CRUD operation testing code (GET, POST, validation errors, collection queries) please refer to 📄 **[CRUD Operation Testing Complete Examples](references/crud-test-examples.md)**

---

## Project Structure Recommendations

```text
tests/
├── Sample.WebApplication.UnitTests/           # Unit tests
├── Sample.WebApplication.Integration.Tests/   # Integration tests
│   ├── Controllers/                           # Controller integration tests
│   │   └── ShippersControllerTests.cs
│   ├── Infrastructure/                        # Test infrastructure
│   │   └── CustomWebApplicationFactory.cs
│   ├── IntegrationTestBase.cs                 # Test base class
│   └── GlobalUsings.cs
└── Sample.WebApplication.E2ETests/            # End-to-end tests
```text

---

## Package Compatibility Troubleshooting

### Common Errors

```text
error CS1061: 'ObjectAssertions' does not contain a definition for 'Be200Ok'
```text

### Solutions

| Base Assertion Library | Correct Package |
| ---------------------- | --------------- |
| FluentAssertions < 8.0.0 | FluentAssertions.Web |
| FluentAssertions >= 8.0.0 | FluentAssertions.Web.v8 |
| AwesomeAssertions >= 8.0.0 | AwesomeAssertions.Web |

```xml
<!-- Correct: using AwesomeAssertions should install AwesomeAssertions.Web -->
<PackageReference Include="AwesomeAssertions" Version="9.1.0" />
<PackageReference Include="AwesomeAssertions.Web" Version="1.9.6" />
```text

---

## Best Practices

### Should Do ✅

1. **Separate Test Projects**: Integration test projects should be separate from unit tests
2. **Test Data Isolation**: Each test case has independent data preparation and cleanup
3. **Use Base Classes**: Shared configuration and helper methods in base classes
4. **Explicit Naming**: Use three-part naming (Method_Scenario_Expected)
5. **Appropriate Test Scope**: Focus on integration points, don't over-test

### Should Avoid ❌

1. **Mixed Test Types**: Don't put unit tests and integration tests in the same project
2. **Test Dependencies**: Each test should be independent, not dependent on other tests' execution order
3. **Over-mocking**: Integration tests should use real components as much as possible
4. **Ignoring Cleanup**: Clean up test data after tests complete
5. **Hard-coded Data**: Use factory methods or Builder pattern to create test data

---

## Related Skills

- `unit-test-fundamentals` - Unit testing fundamentals
- `nsubstitute-mocking` - Mocking with NSubstitute
- `awesome-assertions-guide` - AwesomeAssertions fluent assertions
- `testcontainers-database` - Containerized database testing with Testcontainers

---

## Reference Resources

### Original Articles

This skill content is distilled from the "Old School Software Engineer's Testing Practice - 30 Day Challenge" article series:

- **Day 19 - Integration Testing Introduction: Basic Architecture and Application Scenarios**
  - Article: https://ithelp.ithome.com.tw/articles/10376335
  - Sample code: https://github.com/kevintsengtw/30Days_in_Testing_Samples/tree/main/day19

### Official Documentation

- [ASP.NET Core Integration Testing Documentation](https://docs.microsoft.com/aspnet/core/test/integration-tests)
- [WebApplicationFactory Usage Guide](https://docs.microsoft.com/aspnet/core/test/integration-tests#basic-tests-with-the-default-webapplicationfactory)
- [AwesomeAssertions.Web GitHub](https://github.com/AwesomeAssertions/AwesomeAssertions.Web)
- [AwesomeAssertions.Web NuGet](https://www.nuget.org/packages/AwesomeAssertions.Web)

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Patterns and techniques for evaluating and improving AI agent outputs.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

Comprehensive AI prompt engineering safety review and improvement prompt. Analyzes prompts for safety, bias, security vulnerabilities, and effectiveness while providing detailed improvement recommendations.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

Use when user requests research requiring multiple sources, comprehensive analysis, or synthesis across topics - technical research, domain knowledge gathering, market analysis, or learning about complex subjects

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

deep-wiki

무료

AI-powered wiki generation for code repositories with commands, agents, and skills

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

Use when building .NET 10 or C# 14 applications; when using minimal APIs, modular monolith patterns, or feature folders; when implementing HTTP resilience, Options pattern, Channels, or validation; when seeing outdated patterns like old extension method syntax

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

Implements accessible .NET UI. SemanticProperties, ARIA, AutomationPeer, testing per platform.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

rudironsoni/Synaxis22026년 3월 17일 업데이트

rudironsoni의 스킬 모두 보기

이 스킬의 문제 신고