首页 > 解决方案 > 如何排除对新子类的访问,同时仍允许从另一个程序集进行测试?

问题描述

我遇到了一种我不确定在 C# 中是否可行的情况,但我还是想问一下。我需要从具有两个具体子类的类库中公开一个类型,并且我不希望用户能够创建子类的新实例 - 但是,如果我将所有内容都设为内部,那么用户将无法为测试目的创建实例。

我目前拥有的是这样的(所有示例都已简化,但要理解这个想法):

public abstract class Result
{
    internal Result() { }
}

internal class SuccessResult : Result { }

internal class FailureResult<TError> : Result
{
    TError Error { get; init; }
}

库的用户可以访问如下所示的一对接口,一次提供一个接口,从而限制可以创建的结果类型:

public interface IPartialResultFactory
{
    Result Success();
}

public interface IResultFactory : IPartialResultFactory
{
    Result Error<TError>(TError error);
}

执行上述操作,我可以确保只有有权访问该程序集的内部类的代码才能创建 的新子类型Result,但是当有人测试他们与库的集成时,他们将无法轻松测试代码,因为他们不能创建Result类的新实例。

我目前正在使用的解决方案是重新定义Result类,如下所示:

public abstract class Result
{
    internal Result() { }

    public static Result Success() => new SuccessResult();

    public static Result Error<TError>(TError error) => new ErrorResult<TError>(error);
}

然而这有两个问题:

所以,总而言之,有没有一种方法可以让用户测试与我的代码的集成,创建 的实例Result,然后限制如何为实际集成创建这些实例?

标签: c#interfaceintegration-testing.net-assemblyencapsulation

解决方案


我不确定您是否使用公共工厂接口来创建您的 Result 类,但这样的工作是否可行。

我发现通常最好提供固定的接口定义而不是具有填充方法的类类型 - 请记住,完全抽象的类与接口相同。

使用接口类型,库用户可以轻松地生成他们自己的单元测试,这些单元测试使用它们的 Mock 实现。

我希望提供两种类型Result,每种类型由以下接口定义:

public interface IResultBase
{
    SupportedResultType Type { get; }
    IResult Result { get; }
}

public interface IResultComplex : IResultBase
{
    IError Error { get; }
}

在哪里

public enum SupportedResultType
{
    Base,
    Complex
}

我希望在我的Result类型中传输的数据:

public interface IResult
{
    string SomeValue { get;  }
    bool Success { get;  }
}

public class Result : IResult
{
    public Result(string someValue, bool success)
    {
        SomeValue = someValue;
        Success = success;
    }
    public string SomeValue { get; }
    public bool Success { get; }
}

public interface IError
{
    string ErrorMessage { get; }
}

public class Error : IError
{
    public Error(string errorMessage)
    {
        ErrorMessage = errorMessage;
    }
    public string ErrorMessage { get; }
}

只要观察到接口类型,它背后的具体类就无关紧要,代码库的用户仍然可以使用它们。

这是我的意思的一个例子。

我使用工厂创建它们,但实际上您的库用户可以使用返回具体实现的公共方法。

public sealed class ResultFactory
{
    private readonly Dictionary<SupportedResultType, IResultLocator> _resultLocators;

    private ResultFactory()
    {
        _resultLocators = new Dictionary<SupportedResultType, IResultLocator>();

        foreach (SupportedResultType resultType in System.Enum.GetValues(typeof(SupportedResultType)))
        {
            IResultLocator creator;
            switch (resultType)
            {
                case SupportedResultType.Base:
                    creator = new ResultBaseLocator();
                    break;

                case SupportedResultType.Complex:
                    creator = new ResultComplexLocator();
                    break;

                default:
                    throw new ArgumentOutOfRangeException();
            }

            _resultLocators.Add(resultType, creator);

        }
    }

    private static readonly Lazy<ResultFactory> LazyInstantiation =
        new Lazy<ResultFactory>(() => new ResultFactory());

    public static ResultFactory Initialise() => LazyInstantiation.Value;

    public IResultBase Create(SupportedResultType resultType) =>
        _resultLocators[resultType].Get();

}

工厂实际上并没有创建具体的实例,这是由一个IResultLocator实例执行的,这允许我们在以后扩展实例的数量,IResultBase同时IResultCompex最大限度地减少对工厂的更改。

internal class ResultBaseLocator : IResultLocator
{
    public IResultBase Get()
    {
        return new ResultBase();
    }
}

internal class ResultComplexLocator : IResultLocator
{
    public IResultBase Get()
    {
        return new ResultComplex();
    }
}

对于我的示例,这里是具体的实现:

internal class ResultBase : IResultBase
{
    public SupportedResultType Type { get; } = SupportedResultType.Base;
    public IResult Result { get; } = new Result("ResultBase", true);
}

internal class ResultComplex : IResultComplex
{
    public SupportedResultType Type { get; } = SupportedResultType.Complex;
    public IResult Result { get; } = new Result("ResultComplex", false );
    public IError Error { get; } = new Error("Error message");
}

我将上述类放入类库中,然后创建一个引用它的控制台应用程序。控制台应用程序中的以下代码演示了它们。

    static void Main(string[] args)
    {
        foreach (SupportedResultType resultType in System.Enum.GetValues(typeof(SupportedResultType)))
        {
            IResultBase result = ResultFactory.Initialise().Create(resultType);
            Console.WriteLine($"Result is type {result.Type}");
            Console.WriteLine($"Result.SomeValue = {result.Result.SomeValue}");
            Console.WriteLine($"Result.Success = {result.Result.Success}");

            if (result.Type == SupportedResultType.Complex)
            {
                Console.WriteLine($"Result.Error.ErrorMessage = {((IResultComplex)result).Error.ErrorMessage}");
            }
           
            if (result.GetType().GetInterfaces().Contains(typeof(IResultComplex)))
            {
                Console.WriteLine($"Result is IResultComplex type");
            }

        }
    }

输出如下所示:

在此处输入图像描述


推荐阅读