使用 Moq 和 xUnit 对 ASP.NET Core Web API 进行单元测试(控制器 + 服务)
发布: (2025年12月8日 GMT+8 01:40)
4 min read
原文: Dev.to
Source: Dev.to
什么是 Moq?
Moq 允许你用轻量级的测试替身替换真实的依赖,从而在隔离的环境中测试逻辑。
核心方法
Setup()→ 定义模拟行为ReturnsAsync()→ 为异步方法返回值ThrowsAsync()→ 模拟失败情况Verify()→ 断言某个依赖被调用
本指南的所有示例都遵循 Mock → Execute → Validate(模拟 → 执行 → 验证)模式。
为什么要测试 Web API 控制器?
控制器负责处理 HTTP 请求并返回响应。测试可以确保:
- ✅ 正确的 HTTP 状态码(200、404、400、201)
- ✅ 服务被以正确的参数调用
- ✅ 验证逻辑正常工作
- ✅ 错误得到妥善处理
原则: 控制器应当保持 薄(thin)——它们负责协调,而不是实现业务逻辑。
初始化(约 2 分钟)
dotnet new xunit -n YourApi.Tests
cd YourApi.Tests
dotnet add package Moq --version 4.20.72
dotnet add package FluentAssertions --version 6.12.2
dotnet add reference ../YourApi/YourApi.csproj
模式:Mock → Execute → Validate
示例控制器(ProductsController)
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly ILogger _logger;
public ProductsController(IProductService productService, ILogger logger)
{
_productService = productService;
_logger = logger;
}
[HttpGet("{id}")]
public async Task> GetById(int id)
{
var product = await _productService.GetProductAsync(id);
if (product == null)
return NotFound(new { message = $"Product with ID {id} not found" });
var response = new ProductResponse
{
Id = product.Id,
Name = product.Name,
SKU = product.SKU,
Description = product.Description,
Price = product.Price,
IsActive = product.IsActive,
Category = product.Category != null
? new CategoryResponse { Id = product.Category.Id, Name = product.Category.Name }
: new CategoryResponse()
};
return Ok(response);
}
[HttpPost]
public async Task> Create([FromBody] CreateProductRequest request)
{
try
{
var product = await _productService.CreateProductAsync(request);
var response = new ProductResponse
{
Id = product.Id,
Name = product.Name,
SKU = product.SKU,
Price = product.Price,
IsActive = product.IsActive
};
return CreatedAtAction(nameof(GetById), new { id = product.Id }, response);
}
catch (InvalidOperationException ex)
{
return Conflict(new { message = ex.Message });
}
}
}
测试控制器
通过模拟服务、调用动作方法,并对 ActionResult(状态码 + 响应体)进行断言来测试控制器。
测试类初始化
using CommonComps.Models;
using CommonComps.Models.Requests;
using CommonComps.Models.Responses;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Moq;
using ProductWebAPI.Controllers;
using ProductWebAPI.Services;
using Xunit;
public class ProductsControllerTests
{
private readonly Mock _mockService;
private readonly Mock> _mockLogger;
private readonly ProductsController _controller;
public ProductsControllerTests()
{
_mockService = new Mock();
_mockLogger = new Mock>();
_controller = new ProductsController(_mockService.Object, _mockLogger.Object);
}
}
GET 接口测试
[Fact]
public async Task GetById_ReturnsOk_WhenProductExists()
{
// Arrange
var product = new Product
{
Id = 1,
Name = "Laptop",
SKU = "LAP-001",
Price = 999.99m,
IsActive = true,
Category = new Category { Id = 1, Name = "Electronics" }
};
_mockService.Setup(s => s.GetProductAsync(1))
.ReturnsAsync(product);
// Act
var result = await _controller.GetById(1);
// Assert
var okResult = result.Result.Should().BeOfType().Subject;
var response = okResult.Value.Should().BeOfType().Subject;
response.Id.Should().Be(1);
response.Name.Should().Be("Laptop");
_mockService.Verify(s => s.GetProductAsync(1), Times.Once);
}
[Fact]
public async Task GetById_ReturnsNotFound_WhenProductDoesNotExist()
{
// Arrange
_mockService.Setup(s => s.GetProductAsync(999))
.ReturnsAsync((Product?)null);
// Act
var result = await _controller.GetById(999);
// Assert
result.Result.Should().BeOfType();
}
POST 接口测试
[Fact]
public async Task Create_ReturnsCreated_WhenSuccessful()
{
// Arrange
var request = new CreateProductRequest
{
Name = "New Product",
SKU = "NEW-001",
Price = 49.99m,
CategoryId = 1
};
var createdProduct = new Product
{
Id = 42,
Name = "New Product",
SKU = "NEW-001",
Price = 49.99m,
IsActive = true
};
_mockService.Setup(s => s.CreateProductAsync(It.IsAny()))
.ReturnsAsync(createdProduct);
// Act
var result = await _controller.Create(request);
// Assert
var createdResult = result.Result.Should().BeOfType().Subject;
createdResult.StatusCode.Should().Be(201);
createdResult.ActionName.Should().Be(nameof(ProductsController.GetById));
var response = createdResult.Value.Should().BeOfType().Subject;
response.Id.Should().Be(42);
_mockService.Verify(s => s.CreateProductAsync(It.IsAny()), Times.Once);
}
[Fact]
public async Task Create_ReturnsConflict_WhenDuplicateSKU()
{
// Arrange
var request = new CreateProductRequest { Name = "Test", SKU = "DUP-001" };
_mockService.Setup(s => s.CreateProductAsync(It.IsAny()))
.ThrowsAsync(new InvalidOperationException("Product with SKU DUP-001 already exists"));
// Act
var result = await _controller.Create(request);
// Assert
result.Result.Should().BeOfType();
}

Moq 的关键技巧
设置返回值
// 返回特定值
_mockService.Setup(s => s.GetProductAsync(1)).ReturnsAsync(product);
// 返回 null
_mockService.Setup(s => s.GetProductAsync(999)).ReturnsAsync((Product?)null);
// 对任意参数返回
_mockService.Setup(s => s.GetProductAsync(It.IsAny())).ReturnsAsync(product);