You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为纯REST API应用编写规范的集成测试?

纯REST API集成测试规范写法指南

你已经基于WebApplicationFactory<Program>实现了基础的GET接口测试,下面针对POST/PUT/DELETE接口给出简洁且规范的集成测试写法,同时优化现有测试逻辑:

一、优化现有GET测试

首先修正硬编码URL的问题,WebApplicationFactory创建的客户端会自动处理基础地址,无需指定localhost和端口,改用相对路径更灵活:

public class CategoryTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    // 构造函数初始化客户端,避免每个测试重复创建
    public CategoryTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = _factory.CreateClient();
    }

    [Theory]
    [InlineData("/api/Categories")]
    [InlineData("/api/Categories/88624B65-EF01-4463-79D4-08DAA478AD6E")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Act
        var response = await _client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal("application/json; charset=utf-8",
            response.Content.Headers.ContentType.ToString());
    }
}

二、POST接口测试

POST测试需要构造请求体,验证创建成功后的状态码、返回数据及数据库写入结果:

[Fact]
public async Task Post_Category_ReturnsCreatedAndValidData()
{
    // Arrange
    var newCategory = new CategoryDTO
    {
        Name = "Test Category",
        // 填充其他必要字段
    };
    var jsonContent = new StringContent(
        JsonSerializer.Serialize(newCategory),
        Encoding.UTF8,
        "application/json");

    // Act
    var response = await _client.PostAsync("/api/Categories", jsonContent);

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.Created, response.StatusCode);

    // 验证返回的资源URI和数据
    var createdCategory = await JsonSerializer.DeserializeAsync<CategoryDTO>(
        await response.Content.ReadAsStreamAsync(),
        new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    Assert.NotNull(createdCategory);
    Assert.Equal(newCategory.Name, createdCategory.Name);

    // 额外验证数据库中存在该数据(可选,若使用真实测试库)
    using var scope = _factory.Services.CreateScope();
    var dbContext = scope.ServiceProvider.GetRequiredService<YourDbContext>();
    var dbCategory = await dbContext.Categories.FindAsync(createdCategory.Id);
    Assert.NotNull(dbCategory);
}

// 测试非法请求场景
[Fact]
public async Task Post_InvalidCategory_ReturnsBadRequest()
{
    // Arrange:构造不符合校验规则的DTO(比如缺少必填字段)
    var invalidCategory = new CategoryDTO();
    var jsonContent = new StringContent(
        JsonSerializer.Serialize(invalidCategory),
        Encoding.UTF8,
        "application/json");

    // Act
    var response = await _client.PostAsync("/api/Categories", jsonContent);

    // Assert
    Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

三、PUT接口测试

PUT测试需要先确保目标资源存在,再验证更新后的状态及数据一致性:

[Fact]
public async Task Put_Category_ReturnsNoContentAndUpdatesData()
{
    // Arrange:先创建一个待更新的分类
    var initialCategory = new CategoryDTO { Name = "Initial Name" };
    var createContent = new StringContent(
        JsonSerializer.Serialize(initialCategory),
        Encoding.UTF8,
        "application/json");
    var createResponse = await _client.PostAsync("/api/Categories", createContent);
    var createdCategory = await JsonSerializer.DeserializeAsync<CategoryDTO>(
        await createResponse.Content.ReadAsStreamAsync(),
        new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    Assert.NotNull(createdCategory);

    // 构造更新后的DTO
    var updatedCategory = new CategoryDTO
    {
        Id = createdCategory.Id,
        Name = "Updated Name",
        // 其他字段更新
    };
    var updateContent = new StringContent(
        JsonSerializer.Serialize(updatedCategory),
        Encoding.UTF8,
        "application/json");

    // Act
    var response = await _client.PutAsync($"/api/Categories/{createdCategory.Id}", updateContent);

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);

    // 验证数据已更新
    var getResponse = await _client.GetAsync($"/api/Categories/{createdCategory.Id}");
    var fetchedCategory = await JsonSerializer.DeserializeAsync<CategoryDTO>(
        await getResponse.Content.ReadAsStreamAsync(),
        new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    Assert.Equal("Updated Name", fetchedCategory.Name);
}

// 测试更新不存在的分类
[Fact]
public async Task Put_NonExistentCategory_ReturnsNotFound()
{
    // Arrange
    var nonExistentId = Guid.NewGuid();
    var categoryDto = new CategoryDTO { Id = nonExistentId, Name = "Test" };
    var jsonContent = new StringContent(
        JsonSerializer.Serialize(categoryDto),
        Encoding.UTF8,
        "application/json");

    // Act
    var response = await _client.PutAsync($"/api/Categories/{nonExistentId}", jsonContent);

    // Assert
    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

四、DELETE接口测试

DELETE测试验证资源删除后的状态及是否无法再获取:

[Fact]
public async Task Delete_Category_ReturnsNoContentAndRemovesData()
{
    // Arrange:先创建一个待删除的分类
    var newCategory = new CategoryDTO { Name = "To Delete" };
    var createContent = new StringContent(
        JsonSerializer.Serialize(newCategory),
        Encoding.UTF8,
        "application/json");
    var createResponse = await _client.PostAsync("/api/Categories", createContent);
    var createdCategory = await JsonSerializer.DeserializeAsync<CategoryDTO>(
        await createResponse.Content.ReadAsStreamAsync(),
        new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
    Assert.NotNull(createdCategory);

    // Act
    var response = await _client.DeleteAsync($"/api/Categories/{createdCategory.Id}");

    // Assert
    response.EnsureSuccessStatusCode();
    Assert.Equal(HttpStatusCode.NoContent, response.StatusCode);

    // 验证资源已被删除
    var getResponse = await _client.GetAsync($"/api/Categories/{createdCategory.Id}");
    Assert.Equal(HttpStatusCode.NotFound, getResponse.StatusCode);
}

// 测试删除不存在的分类
[Fact]
public async Task Delete_NonExistentCategory_ReturnsNotFound()
{
    // Arrange
    var nonExistentId = Guid.NewGuid();

    // Act
    var response = await _client.DeleteAsync($"/api/Categories/{nonExistentId}");

    // Assert
    Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}

五、保持测试简洁的关键技巧

  • 复用客户端:在构造函数中初始化HttpClient,避免每个测试重复创建
  • 使用相对路径:依赖WebApplicationFactory自动处理基础地址,避免硬编码端口和域名
  • 提取通用逻辑:将序列化Json、创建测试数据等重复代码封装成私有方法
  • 隔离测试环境:使用内存数据库或临时数据库(如SQLite内存模式),确保测试之间互不干扰
  • 聚焦核心断言:每个测试只验证一个核心场景,避免过度断言

附:你提供的CategoryController核心代码

[HttpGet]
public async Task<ActionResult<IEnumerable<CategoryDTO>>> GetCategories()
{
    _logger.LogInformation("Getting all categories.");
    var categoriesDTO = new List<CategoryDTO>();
    var categories = await _context.Categories
        .ToListAsync();
    if (categories == null)
    {
        _logger.LogError("No categories were found in current context.");
        return NotFound();
    }
    foreach (var category in categories)
    {
        categoriesDTO.Add(_mapper.Map<CategoryDTO>(category));
    }

    _logger.LogInformation("GetIncomes method execudeted successfully, and the list of DTO returned.");

    return categoriesDTO;
}

[HttpGet("{id}")]
public async Task<ActionResult<CategoryDTO>> GetCategory(Guid id)
{
    _logger.LogInformation($"Getting operation by {id} id in GetCategory method.");
    var category = await _context.Categories
        .Where(x => x.Id == id)
        .FirstOrDefaultAsync();
    if (category == null)
    {
        _logger.LogError($"Category {id} was not found in the current context.");
        return NotFound($"Category Id: {id}");
    }

    var categoryDTO = _mapper.Map<CategoryDTO>(category);

    _logger.LogInformation($"GetCategory method execudeted successfully, and the id: {id} category returned.");

    return categoryDTO;
}

[HttpPut("{id}")]
public async Task<ActionResult> PutCategory(Guid id, CategoryDTO categoryDTO)
{
    var category = _context.Categories.FirstOrDefault(x => x.Id == id);
    if (category == null)
    {
        _logger.LogError($"Update category id: {id} was not found.");
        return NotFound();
    }

    _mapper.Map<CategoryDTO, Category>(categoryDTO, category);
    _context.Entry(category).State = EntityState.Modified;

    try
    {
        _logger.LogInformation($"Trying to save updated category {id} into the Database.");
        await _context.SaveChangesAsync();
        _logger.LogInformation($"Updated category id: {id} saved into the Database successfully.");
    }
    catch (DbUpdateConcurrencyException ex)
    {
        if (!CategoryExists(id))
        {
            _logger.LogError($"Category id: {id} does not exist.");
            return NotFound($"Category id: {id}");
        }
        else
        {
            _logger.LogError(ex.Message);
            return BadRequest(new { message = ex.Message });
        }
    }
    return NoContent();
}

内容的提问来源于stack exchange,提问作者Palamar66

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 21:40:22