【问题标题】:Unit Testing a WCF Client对 WCF 客户端进行单元测试
【发布时间】:2015-01-20 17:01:26
【问题描述】:

我正在使用当前不使用任何依赖注入的代码,并通过 WCF 客户端进行多个服务调用。

public class MyClass
{
    public void Method()
    {
        try
        {
            ServiceClient client = new ServiceClient();
            client.Operation1();
        }
        catch(Exception ex)
        {
            // Handle Exception
        }
        finally
        {
            client = null;
        }

        try
        {
            ServiceClient client = new ServiceClient();
            client.Operation2();
        }
        catch(Exception ex)
        {
            // Handle Exception
        }
        finally
        {
            client = null;
        }
    }
}

我的目标是通过使用依赖注入使这段代码可单元测试。我的第一个想法是简单地将服务客户端的一个实例传递给类构造函数。然后在我的单元测试中,我可以创建一个模拟客户端用于测试目的,它不会向 Web 服务发出实际请求。

public class MyClass
{
    IServiceClient client;

    public MyClass(IServiceClient client)
    {
        this.client = client;
    }

    public void Method()
    {
        try
        {
            client.Operation1();
        }
        catch(Exception ex)
        {
            // Handle Exception
        } 

        try
        {
            client.Operation2();
        }

        catch(Exception ex)
        {
            // Handle Exception
        }
    }
}

但是,根据来自以下问题的信息,我意识到这会以影响其原始行为的方式更改代码:Reuse a client class in WCF after it is faulted

在原始代码中,如果对 Operation1 的调用失败并且客户端处于故障状态,则会创建一个新的 ServiceClient 实例,并且仍会调用 Operation2。 在更新后的代码中,如果对 Operation1 的调用失败,则重用同一个客户端调用 Operation2,但如果客户端处于故障状态,则此调用将失败。

是否可以在保持依赖注入模式的同时创建客户端的新实例?我意识到反射可以用来从字符串实例化一个类,但我觉得反射不是解决这个问题的正确方法。

【问题讨论】:

  • 创建客户端的唯一问题是什么?根据我的经验,您实际上希望返回一个模拟客户端,基于其业务接口而不是生成的客户端类(因为生成的类的方法并不总是虚拟的)。此外,真正的客户端需要在使用后进行处理(我在您的代码中没有看到),这是业务接口通常不支持的。

标签: c# wcf unit-testing


【解决方案1】:

你需要注入factory而不是实例本身:

public class ServiceClientFactory : IServiceClientFactory
{
    public IServiceClient CreateInstance()
    {
        return new ServiceClient();
    }
}

然后在MyClass 中,您只需在每次需要时使用工厂来获取实例:

// Injection
public MyClass(IServiceClientFactory serviceClientFactory)
{
    this.serviceClientFactory = serviceClientFactory;
}

// Usage
try
{
    var client = serviceClientFactory.CreateInstance();
    client.Operation1();
}

或者,您可以使用Func<IServiceClient>委托注入返回此类客户端的函数,这样您就可以避免创建额外的类和接口:

// Injection
public MyClass(Func<IServiceClient> createServiceClient)
{
    this.createServiceClient = createServiceClient;
}

// Usage
try
{
    var client = createServiceClient();
    client.Operation1();
}

// Instance creation
var myClass = new MyClass(() => new ServiceClient());

在您的情况下,Func&lt;IServiceClient&gt; 应该足够了。一旦实例创建逻辑变得更加复杂,就需要重新考虑显式实现工厂了。

【讨论】:

    【解决方案2】:

    我过去所做的是有一个通用客户端(使用 Unity 进行“拦截”),它根据服务的业务接口从 ChannelFactory 为每次调用创建一个新连接,并在每次调用后关闭该连接,决定关于是否根据返回异常或正常响应来指示连接发生故障。 (见下文。)

    我使用这个客户端的真实代码只是请求一个实现业务接口的实例,它将获得这个通用包装器的一个实例。返回的实例不需要根据是否返回异常来处理或区别对待。要获得服务客户端(使用下面的包装器),我的代码是:var client = SoapClientInterceptorBehavior&lt;T&gt;.CreateInstance(new ChannelFactory&lt;T&gt;("*")),它通常隐藏在注册表中或作为构造函数参数传入。因此,在您的情况下,我最终会得到var myClass = new MyClass(SoapClientInterceptorBehavior&lt;IServiceClient&gt;.CreateInstance(new ChannelFactory&lt;IServiceClient&gt;("*")));(您可能希望将整个调用以在您自己的某些工厂方法中创建实例,只需要 IServiceClient 作为输入类型,以使其更具可读性。;- ))

    在我的测试中,我可以只注入一个模拟的服务实现,并测试是否调用了正确的业务方法以及是否正确处理了它们的结果。

        /// <summary>
        /// IInterceptionBehavior that will request a new channel from a ChannelFactory for each call,
        /// and close (or abort) it after each call.
        /// </summary>
        /// <typeparam name="T">business interface of SOAP service to call</typeparam>
        public class SoapClientInterceptorBehavior<T> : IInterceptionBehavior 
        {
            // create a logger to include the interface name, so we can configure log level per interface
            // Warn only logs exceptions (with arguments)
            // Info can be enabled to get overview (and only arguments on exception),
            // Debug always provides arguments and Trace also provides return value
            private static readonly Logger Logger = LogManager.GetLogger(LoggerName());
    
        private static string LoggerName()
        {
            string baseName = MethodBase.GetCurrentMethod().DeclaringType.FullName;
            baseName = baseName.Remove(baseName.IndexOf('`'));
            return baseName + "." + typeof(T).Name;
        }
    
        private readonly Func<T> _clientCreator;
    
        /// <summary>
        /// Creates new, using channelFactory.CreatChannel to create a channel to the SOAP service.
        /// </summary>
        /// <param name="channelFactory">channelfactory to obtain connections from</param>
        public SoapClientInterceptorBehavior(ChannelFactory<T> channelFactory)
                    : this(channelFactory.CreateChannel)
        {
        }
    
        /// <summary>
        /// Creates new, using the supplied method to obtain a connection per call.
        /// </summary>
        /// <param name="clientCreationFunc">delegate to obtain client connection from</param>
        public SoapClientInterceptorBehavior(Func<T> clientCreationFunc)
        {
            _clientCreator = clientCreationFunc;
        }
    
        /// <summary>
        /// Intercepts calls to SOAP service, ensuring proper creation and closing of communication
        /// channel.
        /// </summary>
        /// <param name="input">invocation being intercepted.</param>
        /// <param name="getNext">next interceptor in chain (will not be called)</param>
        /// <returns>result from SOAP call</returns>
        public IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
        {
            Logger.Info(() => "Invoking method: " + input.MethodBase.Name + "()");
            // we will not invoke an actual target, or call next interception behaviors, instead we will
            // create a new client, call it, close it if it is a channel, and return its
            // return value.
            T client = _clientCreator.Invoke();
            Logger.Trace(() => "Created client");
            var channel = client as IClientChannel;
            IMethodReturn result;
    
            int size = input.Arguments.Count;
            var args = new object[size];
            for(int i = 0; i < size; i++)
            {
                args[i] = input.Arguments[i];
            }
            Logger.Trace(() => "Arguments: " + string.Join(", ", args));
    
            try
            {
                object val = input.MethodBase.Invoke(client, args);
                if (Logger.IsTraceEnabled)
                {
                    Logger.Trace(() => "Completed " + input.MethodBase.Name + "(" + string.Join(", ", args) + ") return-value: " + val);
                }
                else if (Logger.IsDebugEnabled)
                {
                    Logger.Debug(() => "Completed " + input.MethodBase.Name + "(" + string.Join(", ", args) + ")");
                }
                else
                {
                    Logger.Info(() => "Completed " + input.MethodBase.Name + "()");
                }
    
                result = input.CreateMethodReturn(val, args);
                if (channel != null)
                {
                    Logger.Trace("Closing channel");
                    channel.Close();
                }
            }
            catch (TargetInvocationException tie)
            {
                // remove extra layer of exception added by reflective usage
                result = HandleException(input, args, tie.InnerException, channel);
            }
            catch (Exception e)
            {
                result = HandleException(input, args, e, channel);
            }
    
            return result;
    
        }
    
        private static IMethodReturn HandleException(IMethodInvocation input, object[] args, Exception e, IClientChannel channel)
        {
            if (Logger.IsWarnEnabled)
            {
                // we log at Warn, caller might handle this without need to log
                string msg = string.Format("Exception from " + input.MethodBase.Name + "(" + string.Join(", ", args) + ")");
                Logger.Warn(msg, e);
            }
            IMethodReturn result = input.CreateExceptionMethodReturn(e);
            if (channel != null)
            {
                Logger.Trace("Aborting channel");
                channel.Abort();
            }
            return result;
        }
    
        /// <summary>
        /// Returns the interfaces required by the behavior for the objects it intercepts.
        /// </summary>
        /// <returns>
        /// The required interfaces.
        /// </returns>
        public IEnumerable<Type> GetRequiredInterfaces()
        {
            return new [] { typeof(T) };
        }
    
        /// <summary>
        /// Returns a flag indicating if this behavior will actually do anything when invoked.
        /// </summary>
        /// <remarks>
        /// This is used to optimize interception. If the behaviors won't actually
        ///             do anything (for example, PIAB where no policies match) then the interception
        ///             mechanism can be skipped completely.
        /// </remarks>
        public bool WillExecute
        {
            get { return true; }
        }
    
        /// <summary>
        /// Creates new client, that will obtain a fresh connection before each call
        /// and closes the channel after each call.
        /// </summary>
        /// <param name="factory">channel factory to connect to service</param>
        /// <returns>instance which will have SoapClientInterceptorBehavior applied</returns>
        public static T CreateInstance(ChannelFactory<T> factory)
        {
            IInterceptionBehavior behavior = new SoapClientInterceptorBehavior<T>(factory);
            return (T)Intercept.ThroughProxy<IMy>(
                      new MyClass(),
                      new InterfaceInterceptor(),
                      new[] { behavior });
        }
    
        /// <summary>
        /// Dummy class to use as target (which will never be called, as this behavior will not delegate to it).
        /// Unity Interception does not allow ONLY interceptor, it needs a target instance
        /// which must implement at least one public interface.
        /// </summary>
        public class MyClass : IMy
        {
        }
        /// <summary>
        /// Public interface for dummy target.
        /// Unity Interception does not allow ONLY interceptor, it needs a target instance
        /// which must implement at least one public interface.
        /// </summary>
        public interface IMy
        {
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2012-08-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-12-07
      • 2016-03-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多