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

