【问题标题】:Unable to return an interface in mocked method when it should be a internal sealed class当它应该是内部密封类时,无法在模拟方法中返回接口
【发布时间】:2019-10-24 18:40:04
【问题描述】:

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

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

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

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

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

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

很遗憾,我们正在测试的方法是这样使用 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 调用,我不会这样做,因此我们需要模拟它才能通过单元测试,因为我们必须模拟我们的客户端。

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

【问题讨论】:

  • 为什么不能模拟IClientChannel?类似var clientChannelMock = new Mock&lt;IClientChannel&gt;(); client.Setup(i =&gt; i.GetInnerChannel()).Returns(clientChannelMock);
  • 我试过了。问题出在我们正在测试的方法中,我们有以下行:using (new OperationContextScope(client.GetInnerChannel())){ OperationContext.Current.OutgoingMessageHeaders.Add( new EquifaxSecurityHeader(appID, username, password)); 问题是 OperationContextScope 抛出错误Invalid IContextChannel passed to OperationContext. Must be either a server dispatching channel or a client proxy channel. 另外,我们使用静态方法来设置标题(不能被嘲笑)。

标签: c# unit-testing moq xunit.net


【解决方案1】:

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

简而言之,如果您有无法模拟的静态、密封或其他第三方类,请将其包装在一个简单的任务中,并在其中编写所有公共方法,然后调用您的第三方类的方法,然后创建一个接口对于包装器。在你的单元测试中,使用接口,在你的普通代码中,使用你的包装类。

【讨论】:

    【解决方案2】:

    这是可能的,无需编写更多的包装器。了解通过添加接口引入的间接原理很重要。添加的每个接口都是隔离抽象并使其外观和感觉不同的机会。

    我在下面附上了重要的 sn-ps,由于预期的简洁,我没有附上整个解决方案。

    类和使用层次的快速解释 - 1. IInnerChannel 是第三方库暴露的接口。 2. IClientChannelWrapper 是为了隐藏调用客户端的内部接口而创建的包装类。 3. ClassUsingChannelWrapper 是调用此逻辑的类,在我们的单元测试中,它的方法将成为我们的 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;
        }
    }
    

    调用包装器实现的类。实现单元测试时,此类将成为您的 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);
        }
    }
    

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

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

    运行这个单元测试打印

    “这是来自模拟对象的测试。”

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

    【讨论】:

      猜你喜欢
      • 2010-10-30
      • 1970-01-01
      • 1970-01-01
      • 2012-07-07
      • 2022-12-09
      • 1970-01-01
      • 1970-01-01
      • 2014-01-11
      • 1970-01-01
      相关资源
      最近更新 更多