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

如何重构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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 07:20:27