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

.NET Core WebAPI中Polly重试策略的XUnit+Moq单元测试咨询

用XUnit+Moq测试Polly重试逻辑及GetProducts接口

核心思路

测试的关键是模拟服务故障场景,验证Polly是否按配置的次数重试,最终在服务恢复时返回正确结果;同时覆盖正常调用无需重试的场景,确保逻辑正确。

1. 直接测试IProductService的重试逻辑

下面是针对Service层的测试示例,假设你对IProductService.GetProductsAsync()配置了针对HttpRequestException的3次重试策略:

using Xunit;
using Moq;
using Polly;
using Polly.Retry;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

public class ProductServiceRetryTests
{
    private readonly Mock<IProductService> _productServiceMock;
    private readonly AsyncRetryPolicy _retryPolicy;

    public ProductServiceRetryTests()
    {
        _productServiceMock = new Mock<IProductService>();
        // 复用生产环境的重试规则:3次重试,每次间隔100ms,仅处理HttpRequestException
        _retryPolicy = Policy
            .Handle<HttpRequestException>()
            .WaitAndRetryAsync(3, retryAttempt => TimeSpan.FromMilliseconds(100));
    }

    [Fact]
    public async Task GetProducts_WhenServiceFails3TimesThenSucceeds_ShouldRetry3Times()
    {
        int callCount = 0;
        // 配置Mock:前3次调用抛异常,第4次返回正常数据
        _productServiceMock.Setup(s => s.GetProductsAsync())
            .ReturnsAsync(() =>
            {
                callCount++;
                if (callCount <= 3)
                    throw new HttpRequestException("临时不可用");
                return new List<Product> { new Product { Id = 1, Name = "测试商品" } };
            });

        // 执行被重试策略包裹的Service调用
        var result = await _retryPolicy.ExecuteAsync(async () => 
            await _productServiceMock.Object.GetProductsAsync());

        // 验证返回结果正确
        Assert.Single(result);
        Assert.Equal("测试商品", result[0].Name);
        // 验证总调用次数:3次重试+1次成功=4次
        _productServiceMock.Verify(s => s.GetProductsAsync(), Times.Exactly(4));
    }

    [Fact]
    public async Task GetProducts_WhenServiceSucceedsImmediately_ShouldNotRetry()
    {
        // 配置Mock:第一次调用就返回成功
        _productServiceMock.Setup(s => s.GetProductsAsync())
            .ReturnsAsync(new List<Product> { new Product { Id = 2, Name = "直接成功商品" } });

        var result = await _retryPolicy.ExecuteAsync(async () => 
            await _productServiceMock.Object.GetProductsAsync());

        Assert.Single(result);
        // 验证仅调用1次
        _productServiceMock.Verify(s => s.GetProductsAsync(), Times.Once);
    }
}

// 你的基础接口和实体定义(示例)
public interface IProductService
{
    Task<List<Product>> GetProductsAsync();
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

关键说明

  • Mock行为控制:通过计数器callCount精准控制Mock的返回/抛出逻辑,模拟服务从故障到恢复的过程。
  • 策略对齐:测试中使用和生产环境完全一致的重试策略,保证测试结果能真实反映实际运行情况。
  • 次数验证:用Mock.Verify直接检查方法调用次数,确认重试逻辑生效。

2. 测试WebAPI Controller层的GetProducts接口

如果要测试Controller的接口(而非直接测试Service),需要确保Controller使用的是带重试策略包裹的Service实例,测试示例如下:

using Xunit;
using Moq;
using Polly;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;

public class ProductsControllerTests
{
    private readonly Mock<IProductService> _productServiceMock;
    private readonly AsyncRetryPolicy _retryPolicy;
    private readonly ProductsController _controller;

    public ProductsControllerTests()
    {
        _productServiceMock = new Mock<IProductService>();
        _retryPolicy = Policy
            .Handle<HttpRequestException>()
            .WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(100));
        
        // 用Polly的PolicyWrapped包裹Mock的Service,模拟DI注入的带重试的实例
        var wrappedService = Policy.WrapAsync(_retryPolicy, _productServiceMock.Object);
        _controller = new ProductsController(wrappedService);
    }

    [Fact]
    public async Task GetProducts_WhenServiceRecoversAfterRetries_ShouldReturnOk()
    {
        int callCount = 0;
        _productServiceMock.Setup(s => s.GetProductsAsync())
            .ReturnsAsync(() =>
            {
                callCount++;
                if (callCount <= 3)
                    throw new HttpRequestException();
                return new List<Product> { new Product { Id = 1, Name = "Controller测试商品" } };
            });

        // 调用Controller接口
        var result = await _controller.GetProducts();

        // 验证返回类型和数据
        var okResult = Assert.IsType<OkObjectResult>(result);
        var products = Assert.IsType<List<Product>>(okResult.Value);
        Assert.Single(products);
        // 验证Service被调用4次
        _productServiceMock.Verify(s => s.GetProductsAsync(), Times.Exactly(4));
    }
}

// 你的Controller定义(示例)
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet("getProducts")]
    public async Task<IActionResult> GetProducts()
    {
        var products = await _productService.GetProductsAsync();
        return Ok(products);
    }
}

关键说明

  • PolicyWrapped实例:通过Policy.WrapAsync将Mock的Service和重试策略包裹,模拟生产环境中DI注入的带重试的Service实例,确保Controller使用的逻辑和生产一致。
  • 结果验证:不仅要验证重试次数,还要验证Controller返回的IActionResult类型正确,数据符合预期。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 18:54:18