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

如何编写C#的渐进式单元测试?以参数校验方法为例

针对参数校验方法的单元测试优化方案

问题背景

我有一段参数校验的C#代码:

public async Task<ValidationResult> ValidateAsync(Request request, Client client, CancellationToken cancellationToken = default)
{
    if (request is null) throw new ArgumentNullException(nameof(request));
    if (client is null) throw new ArgumentNullException(nameof(client));

    if (string.IsNullOrEmpty(request.Parameter1) && request.Action != "merge")
        return MissingParameter(nameof(request.Parameter1));

    if (string.IsNullOrEmpty(request.Parameter2))
        return MissingParameter(nameof(request.Parameter2));

    if (string.IsNullOrEmpty(request.Parameter3))
        return MissingParameter(nameof(request.Parameter3));

    if (string.IsNullOrEmpty(request.Parameter4))
        return MissingParameter(nameof(request.Parameter4));

    if (string.IsNullOrEmpty(request.Parameter5))
        return MissingParameter(nameof(request.Parameter5));

    if (string.IsNullOrEmpty(request.Parameter6))
        return MissingParameter(nameof(request.Parameter6));

    // Validate length of some parameters
    // Later validate for different combinations (some parameters aren't allowed when specific parameters are present)
    // And other validation (e.g. whether client is allowed to requested parameter's values)

    return new() { IsValid = true };
}

编写单元测试时遇到以下痛点:

  • 测试request或client为null的场景很简单,但测试单个参数缺失的场景时,每次都要确保其他所有参数有效,代码重复度极高。
  • 后续测试参数长度、参数组合等逻辑时,同样需要先构造一个所有参数都有效的基础实例,仅修改要测试的那部分逻辑,重复代码过多。
  • 想知道是否可以实现「渐进式」单元测试(即一个测试结束后,下一个测试从该测试的完成点开始执行)。

解决方案

1. 用测试数据构建器消除重复代码

创建一个RequestBuilder类,专门用来快速构造符合要求的Request实例,默认生成所有参数都有效的基础对象,需要修改某个参数时只调用对应的方法即可:

public class RequestBuilder
{
    private string _action = "default";
    private string _parameter1 = "valid_p1";
    private string _parameter2 = "valid_p2";
    private string _parameter3 = "valid_p3";
    private string _parameter4 = "valid_p4";
    private string _parameter5 = "valid_p5";
    private string _parameter6 = "valid_p6";

    public RequestBuilder WithAction(string action)
    {
        _action = action;
        return this;
    }

    public RequestBuilder WithParameter1(string value)
    {
        _parameter1 = value;
        return this;
    }

    public RequestBuilder WithParameter2(string value)
    {
        _parameter2 = value;
        return this;
    }

    // 为Parameter3-Parameter6添加类似的With方法

    public Request Build()
    {
        return new Request
        {
            Action = _action,
            Parameter1 = _parameter1,
            Parameter2 = _parameter2,
            Parameter3 = _parameter3,
            Parameter4 = _parameter4,
            Parameter5 = _parameter5,
            Parameter6 = _parameter6
        };
    }
}

测试单个参数缺失的场景时,代码会非常简洁:

[Test]
public async Task ValidateAsync_Parameter1Missing_ReturnsError()
{
    // Arrange
    var request = new RequestBuilder().WithParameter1(null).Build();
    var client = new Client(); // 构造有效的Client实例

    // Act
    var result = await validator.ValidateAsync(request, client);

    // Assert
    Assert.That(result.IsValid, Is.False);
    Assert.That(result.ErrorMessage, Does.Contain(nameof(Request.Parameter1)));
}

[Test]
public async Task ValidateAsync_Parameter2Missing_ReturnsError()
{
    // Arrange
    var request = new RequestBuilder().WithParameter2(string.Empty).Build();
    var client = new Client();

    // Act
    var result = await validator.ValidateAsync(request, client);

    // Assert
    Assert.That(result.IsValid, Is.False);
    Assert.That(result.ErrorMessage, Does.Contain(nameof(Request.Parameter2)));
}

2. 用参数化测试批量处理同类场景

对于参数缺失这类逻辑高度一致的测试,可以用参数化测试进一步减少重复。比如用NUnit的TestCase特性:

[TestCase(null, nameof(Request.Parameter1))]
[TestCase("", nameof(Request.Parameter1))]
[TestCase(null, nameof(Request.Parameter2))]
[TestCase("", nameof(Request.Parameter2))]
// 为Parameter3-Parameter6添加对应的TestCase
public async Task ValidateAsync_ParameterMissing_ReturnsError(string parameterValue, string expectedParameterName)
{
    // Arrange
    var builder = new RequestBuilder();
    // 根据参数名设置对应参数的值
    switch(expectedParameterName)
    {
        case nameof(Request.Parameter1):
            builder.WithParameter1(parameterValue);
            break;
        case nameof(Request.Parameter2):
            builder.WithParameter2(parameterValue);
            break;
        // 处理其他参数
    }
    var request = builder.Build();
    var client = new Client();

    // Act
    var result = await validator.ValidateAsync(request, client);

    // Assert
    Assert.That(result.IsValid, Is.False);
    Assert.That(result.ErrorMessage, Does.Contain(expectedParameterName));
}

3. 不要使用「渐进式」单元测试

单元测试的核心原则之一是独立性:每个测试都应该可以单独运行,不依赖其他测试的执行顺序或状态。如果用渐进式测试,一旦某个前置测试失败,后续所有测试都会受影响,而且测试的维护成本会急剧上升——修改一个前置测试可能导致后续所有测试失效。

4. 复杂场景的测试思路

对于参数长度、参数组合这类场景,同样基于RequestBuilder构造基础实例,再修改需要测试的部分:

[Test]
public async Task ValidateAsync_Parameter1TooLong_ReturnsError()
{
    // Arrange
    var request = new RequestBuilder()
        .WithParameter1(new string('a', 101)) // 假设参数1最大长度是100
        .Build();
    var client = new Client();

    // Act
    var result = await validator.ValidateAsync(request, client);

    // Assert
    Assert.That(result.IsValid, Is.False);
    Assert.That(result.ErrorMessage, Does.Contain("Parameter1 exceeds maximum length"));
}

[Test]
public async Task ValidateAsync_ActionMerge_Parameter1Missing_IsAllowed()
{
    // Arrange
    var request = new RequestBuilder()
        .WithAction("merge")
        .WithParameter1(null)
        .Build();
    var client = new Client();

    // Act
    var result = await validator.ValidateAsync(request, client);

    // Assert
    Assert.That(result.IsValid, Is.True); // 因为merge动作允许Parameter1缺失
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 21:12:05