无法 return 应该是内部密封的模拟方法中的接口 class

Unable to return an interface in mocked method when it should be a internal sealed class

我们正在 XUnit 中为我们的 ASP.NET 应用程序项目之一创建单元测试。在项目中,我们试图模拟一个特定的第三方客户端,我们无法访问源代码并且不提供接口(我们可以创建的只是一个客户端对象)。为了解决这个问题,我们编写了一个包装器 class 和一个 class 的接口,这样我们就可以模拟出该客户端所需的功能。这部分都不错

我们在包装器 class 和接口中创建的方法之一是 GetInnerChannel 方法,用于从客户端(我们想要模拟)获取 属性。但是,该方法 return 是 System.ServiceModel.

的 IClientChannel 接口
public IClientChannel GetInnerChannel()
{
        return client.InnerChannel;
}

这似乎无害,但是,在我们的模拟设置中,我们无法创建对我们正在测试的方法有用的假 IClientChannel 对象。这是我们的单元测试代码以供澄清:

client.Setup(i => i.GetInnerChannel()).Returns(GetClientChannel());

在 Returns 调用中,您会看到我们正在 returning 一个方法 return,我们目前暂时将其设置为 null。这是因为我们无法实例化接口。当我深入调试时,我发现在正常操作期间正在发送回接口的对象是一个 System.Runtime.Remoting.Proxies.__TransparentProxy 对象。对 __TransparentProxy class 的一点调查是它是一个内部密封的 class (这意味着我们不能在我们的单元测试代码中实例化它)。

不幸的是,我们正在测试的方法是这样使用 InnerChannel 的:

public List<EquifaxQuestion> GetEquifaxQuestions(User basicUserInfo, IEnumerable<AddressViewModel> Addresses)
    {
        try
        {
            InitialResponse response;
            using (new OperationContextScope(client.GetInnerChannel()))
            {
                OperationContext.Current.OutgoingMessageHeaders.Add(
                    new EquifaxSecurityHeader(appID, username, password));

                response = client.SubmitInitialInteraction(GenerateInitialRequest(basicUserInfo, Addresses));
            }

如果我们可以替换 GetInnerChannel 调用,我不会这样做,因此我们需要模拟它才能通过单元测试,因为我们必须模拟我们的客户端。

还有其他方法可以 return 对 GetInnerChannel() 有用的值或对象吗?我是否错过了模拟设置中的一个步骤?还是 Moq 和其他模拟框架无法完成我需要做的事情?还是我正在尝试针对无法进行单元测试的单元测试的方法?提前谢谢你。

基本上解决这个问题的方法是在 WCF 中使用大量包装器和接口。它很长,但是这个博客 post 做得更好。 https://weblogs.asp.net/cibrax/unit-tests-for-wcf

简而言之,如果你有静态的、密封的或其他第三方 class 你不能模拟,将它包装在一个简单的任务中,所有 public 方法都写在其中并调用它们您的第三方 class 的方法,然后为包装器创建一个接口。在你的单元测试中,使用接口,在你的正常代码中,使用你的包装器 class.

这无需再编写任何包装器即可实现。了解通过添加接口引入的间接原则很重要。添加的每个接口都是一个隔离抽象并使其外观和感觉不同的机会。

我附上了下面的重要片段,由于预期的简洁性,我没有附上整个解决方案。

classes 和用法层次结构的快速解释 - 1. IInnerChannel是第三方库对外暴露的接口。 2. IClientChannelWrapper 是包装器 class 创建的目的是为了隐藏调用客户端的内部接口。 3. ClassUsingChannelWrapper 是调用此逻辑的 class,在我们的单元测试中,它的方法将成为我们的 sut(被测对象)。

代码如下-

IInnerChannel接口声明-

public interface IInnerChannel
{
    string TheInnerChannelMethod();
}

InnerChannel 实现(在您的情况下可能在第三方库中)-

public class InnerChannelImplementation : IInnerChannel
{
    public InnerChannelImplementation()
    {
    }

    public string TheInnerChannelMethod()
    {
        var result = "This is coming from innser channel.";
        Console.WriteLine(result);
        return result;

    }
}

您在内部通道周围创建的包装器 -

public interface IClientChannelWrapper
{
    void DoSomething();
    IInnerChannel GetTheInnerChannelMethod();
}

包装器接口的实现 -

public class ClientChannelWrapperImplementation : IClientChannelWrapper
{
    public ClientChannelWrapperImplementation()
    {
    }

    public void DoSomething()
    {
        Console.WriteLine("The DoSomething Method!");
    }

    public IInnerChannel GetTheInnerChannelMethod()
    {
        InnerChannelImplementation imp = new InnerChannelImplementation();
        return imp;
    }
}

调用包装器实现的 class。这个 class 将成为您实施单元测试时的 SUT -

public class ClassUsingChannelWrapper
{
    IClientChannelWrapper _wrapper;
    public ClassUsingChannelWrapper(IClientChannelWrapper wrapper)
    {
        _wrapper = wrapper;
    }

    public void TheClientChannelConsumerMethod()
    {
        IInnerChannel theChannel = _wrapper.GetTheInnerChannelMethod();
        var result = theChannel.TheInnerChannelMethod();
        Console.WriteLine(result);
    }
}

最后,模拟两个接口行为的单元测试。请注意模拟的客户端通道包装器如何返回模拟的内部通道对象,该对象 returns 预编程值。

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        //Arrange
        Mock<IInnerChannel> innerChannelMock = new Mock<IInnerChannel>();
        innerChannelMock.Setup(i => i.TheInnerChannelMethod()).Returns("This 
        is a test from mocked object.");
        Mock<InterfaceUt.IClientChannelWrapper> mockClientWrapper = new 
        Mock<IClientChannelWrapper>();
        mockClientWrapper.Setup(m => 
        m.GetTheInnerChannelMethod()).Returns(innerChannelMock.Object);

        //Act
        ClassUsingChannelWrapper sut = new 
        ClassUsingChannelWrapper(mockClientWrapper.Object);
        sut.TheClientChannelConsumerMethod();

        //Assert
        innerChannelMock.Verify();
        mockClientWrapper.Verify();

    }
}

运行 这个单元测试打印

"This is a test from mocked object."

本质上,您的单元测试仅针对尝试使用您的界面行为的客户端代码。这不会测试包装器的实现。如果你想实现这一点,你将必须创建一个新的包装器实例 class 而不是模拟对象,并将内部通道的模拟对象提供给它。