如何为纯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
相关产品推荐
相关产品推荐

