【问题标题】:How to exclude access to new subclasses while still allowing testing from another assembly?如何排除对新子类的访问,同时仍然允许从另一个程序集进行测试?
【发布时间】:2021-08-26 10:17:15
【问题描述】:

我遇到了一个我不确定在 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 和它的子类之间的耦合,如果可能的话,我想避免在将来的某个时间点添加其他子类;

  • 其次,这意味着在向用户提供IPartialResultFactory 实例的情况下,他们仍然可以返回ErrorResult 实例,我想限制他们只返回SuccessResult

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

【问题讨论】:

  • 你想创建类似代数数据类型的东西吗?
  • 我不确定我是否真的理解:您想要内部子类但允许从另一个程序集中获取它们的实例以进行测试?在我看来,这是不可能的,正是因为封装级别的限制,这里是内部的......因此即使使用接口,您也无法获得可编译的代码,除非可能绕过 OOP 的约束通过使用反射或 IL 代码注入来实现逻辑。只是说,我从来没有涉足过这种事情。您需要在测试人员和这些类之间提供一个测试公共连接器。
  • @Sweeper 在一定程度上是的,但也不是。在内部,我正在将 Result 的实例转换为 OneOf monad 的简单内部实现(类似于不相交的联合),但是目的是使暴露的部分尽可能地地道,并且在这里使用 OneOf monad 会由于嵌套泛型已经很乱了,我希望在不破坏现有用法的情况下添加更多 Results 的可能性。
  • @Olivier Rogier 我认为我同意我认为这是不可能的,至少在没有一些非常时髦的咖啡怪异的情况下是不可能的。你关于测试连接器的观点很有趣,我不确定这是否会作为 NuGet 包发布,但添加第二个包来测试它可能是一个想法,它允许构造Result 实例。如果有人想特意在非测试代码中使用标记为测试的包,那么就在他们身上。
  • 您是否有理由不为您的 Result 类公开接口类型?

标签: c# interface integration-testing .net-assembly encapsulation


【解决方案1】:

假设单元测试在同一个解决方案中,那么您可以编辑包含 Result 类的项目的项目文件并添加以下内容:

  <ItemGroup>
    <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
      <_Parameter1>ResultImplementationTests</_Parameter1>
    </AssemblyAttribute>
  </ItemGroup>

其中ResultImplementationTests 是单元测试项目的名称,它将测试您的 Result 类。

【讨论】:

  • 我假设 OP 无法提前知道所有可能的客户端测试项目名称,所以这不起作用。
  • 是的,就是这样。这个想法是,这是一个可以任意导入和实现的框架/库,所以我不知道提前将它的内部暴露给哪些程序集。
【解决方案2】:

我不确定您是否使用公共工厂接口来创建您的 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 实例执行的,这允许我们在以后扩展IResultBaseIResultCompex 实例的变体数量,同时最大限度地减少更改去工厂。

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");
            }

        }
    }

输出如下:

【讨论】:

  • 通常我会完全同意你的观点,但是我正在寻找的最终结果并不适合这个实现。目的是让实现者实现一个接口(作为框架的一部分),该接口将提供IPartialResultFacory(因为它们只允许返回SuccessResult 实例)或IResultFactory 实例。使用您建议的实现,用户可以在任何时候返回带有Success == trueSuccess == false 的任意实例,这应该是框架内的行为。
  • 此外,虽然完全抽象的抽象类与接口非常相似,但它们在 C# 中却略有不同。首先,您可以将其所有构造函数设置为internal,这意味着来自同一程序集之外的子类是不可能的,此外,您可以定义私有/受保护方法之类的东西(考虑到一些最新的 C# 接口功能实际上不太适用) .
猜你喜欢
  • 2010-09-08
  • 2010-11-15
  • 1970-01-01
  • 1970-01-01
  • 2011-07-18
  • 2017-08-13
  • 1970-01-01
  • 2020-12-19
  • 2017-11-28
相关资源
最近更新 更多