如何重构C#代码以提升可测试性?附待测试代码示例
针对这段PAR验证代码的测试与重构方案
一、不重构前提下的高效测试方法
你提到的构造“通过前面所有验证的对象”确实是可行方案,但不用每次手动编写重复代码,用测试数据工厂封装重复逻辑就能大幅减少冗余:
1. 编写测试数据工厂类
创建专门生成合法测试对象的类,把通用的合法对象逻辑封装起来:
public static class ParTestDataFactory { public static Client CreateValidClient() { return new Client { GrantTypes = new List<GrantType> { new GrantType { GrantType = GrantTypes.AuthorizationCode } }, Callbacks = new List<Callback> { new Callback { Uri = "https://valid-callback.com" } }, AllowedApiScopes = new List<ApiScope> { new ApiScope { Name = "scope1" }, new ApiScope { Name = "offline_access" } }, AllowedIdentityResources = new List<IdentityResource> { new IdentityResource { Name = "openid" } } }; } public static ParRequest CreateValidParRequest() { return new ParRequest { CallbackUri = "https://valid-callback.com", ApiScope = "scope1", IdentityResource = "openid", Prompt = "none", State = GenerateValidBase64Url(32), Nonce = GenerateValidBase64Url(32), CodeChallenge = GenerateValidBase64Url(32), AuthChallenge = GenerateValidBase64Url(32) }; } private static string GenerateValidBase64Url(int length) { var bytes = new byte[length]; Random.Shared.NextBytes(bytes); return Convert.ToBase64String(bytes).Replace('+', '-').Replace('/', '_').TrimEnd('='); } }
2. 针对性测试示例
比如测试无效prompt的场景,直接基于工厂生成的合法对象修改目标字段即可:
[Test] public void Validate_WhenPromptIsInvalid_ReturnsInvalidPrompt() { // Arrange var client = ParTestDataFactory.CreateValidClient(); var parRequest = ParTestDataFactory.CreateValidParRequest(); parRequest.Prompt = "invalid-prompt"; // 仅修改要测试的字段 // Act var result = new ParRequestValidator().Validate(parRequest, client); // Assert Assert.IsInstanceOf<InvalidPrompt>(result.Errors.First()); }
这种方式不用每次从头构造对象,只需要修改测试点对应的字段,就能确保前置验证都通过,聚焦测试目标。
二、重构代码提升可测试性
如果觉得依赖前置条件的测试不够灵活,可以通过拆分逻辑让每个验证点独立可测:
1. 拆分独立验证器
定义一个验证器接口,把每个验证步骤封装成单独的类,让每个验证逻辑解耦:
public interface IParValidator { RequestValidationResultBase Validate(ParRequest parRequest, Client client); } // GrantType验证器示例 public class ClientGrantTypeValidator : IParValidator { public RequestValidationResultBase Validate(ParRequest parRequest, Client client) { if (!IsStringContainedInArray(GrantTypes.AuthorizationCode, client.GrantTypes.Select(p => p.GrantType).ToArray())) return new(new InvalidGrantType()); return new() { Valid = true }; } private bool IsStringContainedInArray(string value, string[] array) => array.Contains(value); } // Prompt验证器示例 public class PromptValidator : IParValidator { public RequestValidationResultBase Validate(ParRequest parRequest, Client client) { if (!PromptTypes.ValidatePrompt(parRequest.Prompt)) return new(new InvalidPrompt()); return new() { Valid = true }; } }
然后原验证类改成组合这些验证器:
public class ParRequestValidator { private readonly IEnumerable<IParValidator> _validators; public ParRequestValidator(IEnumerable<IParValidator> validators) { _validators = validators; } public RequestValidationResultBase Validate(ParRequest parRequest, Client client) { if (client is null) throw new ArgumentNullException(nameof(client)); foreach (var validator in _validators) { var result = validator.Validate(parRequest, client); if (!result.Valid) return result; } // 建议将apiScope修改逻辑移到单独的转换类,避免验证逻辑带副作用 if (!string.IsNullOrEmpty(parRequest.ApiScope) && parRequest.ApiScope.Split(' ').Contains("offline_access") && (string.IsNullOrEmpty(parRequest.Prompt) || !parRequest.Prompt.Split(' ').Contains(PromptTypes.Consent))) parRequest.ApiScope = parRequest.ApiScope.Replace("offline_access", string.Empty).Trim(); return new() { Valid = true }; } }
这样每个验证器都可以单独测试,比如测试PromptValidator时完全不用管其他验证逻辑:
[Test] public void PromptValidator_WhenPromptIsInvalid_ReturnsInvalidPrompt() { // Arrange var validator = new PromptValidator(); var parRequest = new ParRequest { Prompt = "invalid-prompt" }; var client = new Client(); // Act var result = validator.Validate(parRequest, client); // Assert Assert.IsInstanceOf<InvalidPrompt>(result.Errors.First()); }
2. 其他重构建议
- 移除入参修改副作用:原代码直接修改
parRequest.ApiScope会导致测试对象污染,建议改成返回新的ParRequest实例,或者把这一步作为验证后的转换逻辑单独处理。 - 提取公共工具方法:把
IsStringContainedInArray、IsFirstArrayContainedInSecondArray这类通用逻辑提取到ValidationHelper静态类中,单独测试工具方法的正确性,确保底层逻辑无问题。
总结
- 不重构的话,测试数据工厂能高效解决“构造前置条件合法对象”的问题,避免重复代码;
- 重构的话,拆分独立验证器能让每个测试点完全独立,更易维护和扩展;
- 私有方法无需纠结模拟,要么通过测试调用场景间接验证,要么提取成公共工具类单独测试——模拟私有方法本身就是反模式,说明代码需要拆分。
内容的提问来源于stack exchange,提问作者Szyszka947
相关产品推荐
相关产品推荐

