简体   繁体   English

单元测试WCF客户端

[英]Unit Testing a WCF Client

I am working with code that currently does not use any dependency injection, and makes multiple service calls through a WCF client. 我正在使用当前不使用任何依赖注入的代码,并通过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;
        }
    }
}

My goal is to make this code unit-testable through the use of dependency injection. 我的目标是通过使用依赖注入使这个代码单元可测试。 My first thought was to simply pass an instance of the service client to the class constructor. 我的第一个想法是简单地将服务客户端的实例传递给类构造函数。 Then in my unit tests, I can create a mock client for testing purposes that does not make actual requests to the web service. 然后在我的单元测试中,我可以创建一个模拟客户端用于测试目的,但不会向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
        }
    }
}

However, I realized that this changes the code in a way that affects its original behavior, based on the information from this question: Reuse a client class in WCF after it is faulted 但是,我意识到这会以一种影响其原始行为的方式更改代码,基于此问题的信息: 在WCF出现故障后重新使用WCF中的客户端类

In the original code, if the call to Operation1 fails and the client is put in a faulted state, a new instance of ServiceClient is created, and Operation2 will still be called. 在原始代码中,如果对Operation1的调用失败并且客户端处于故障状态,则会创建一个新的ServiceClient实例,并且仍将调用Operation2。 In the updated code, if the call to Operation1 fails, the same client is reused to call Operation2, but this call will fail if the client is in a faulted state. 在更新的代码中,如果对Operation1的调用失败,则重用相同的客户端来调用Operation2,但如果客户端处于故障状态,则此调用将失败。

Is it possible to create a new instance of the client while keeping the dependency injection pattern? 是否可以在保持依赖注入模式的同时创建客户端的新实例? I realize that reflection can be used to instantiate a class from a string, but I feel like reflection isn't the right way to go about this. 我意识到反射可以用来从字符串中实例化一个类,但我觉得反射不是解决这个问题的正确方法。

You need to inject factory rather than instance itself: 您需要注入工厂而不是实例本身:

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

Then in MyClass you simply use factory to get instance each time it is required: 然后在MyClass您只需使用factory来在每次需要时获取实例:

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

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

Alternatively, you can inject function returning such client using Func<IServiceClient> delegate so that you can avoid creating extra class and interface: 或者,您可以使用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());

In your case Func<IServiceClient> should be sufficient. 在你的情况下, Func<IServiceClient>应该足够了。 Once instance creation logic gets more complicated it would be a time reconsider explicitly implemented factory. 一旦实例创建逻辑变得更复杂,那么将是重新考虑显式实现工厂的时间。

What I did in the past was have a generic client (with 'interception' using Unity) which creates a new connection from the ChannelFactory, based on the service's business interface, for each call and close that connection after each call, deciding on whether to indicate the connection is faulted based on whether an exception or normal response was returned. 我过去所做的是有一个通用客户端(使用Unity进行'拦截'),它根据服务的业务接口从ChannelFactory创建一个新连接,用于每次调用并在每次调用后关闭该连接,决定是否根据是否返回异常或正常响应来指示连接出现故障。 (See below.) (见下文。)

My real code using this client just requests an instance implementing the business interface and it will get an instance of this generic wrapper. 我使用此客户端的真实代码只是请求实现业务接口的实例,它将获得此通用包装器的实例。 The instance returned does not need to be disposed of, or treated differently based on whether an exception was returned. 返回的实例不需要根据是否返回异常进行处理或处理。 To get a service client (using the wrapper below) my code does: var client = SoapClientInterceptorBehavior<T>.CreateInstance(new ChannelFactory<T>("*")) , which is usually hidden in a registry or passed in as a constructor argument. 要获取服务客户端(使用下面的包装器),我的代码执行: var client = SoapClientInterceptorBehavior<T>.CreateInstance(new ChannelFactory<T>("*")) ,它通常隐藏在注册表中或作为构造函数传入论点。 So in your case I would end up with var myClass = new MyClass(SoapClientInterceptorBehavior<IServiceClient>.CreateInstance(new ChannelFactory<IServiceClient>("*"))); 所以在你的情况下,我最终会得到var myClass = new MyClass(SoapClientInterceptorBehavior<IServiceClient>.CreateInstance(new ChannelFactory<IServiceClient>("*"))); (you probably want to put the whole call to create the instance in some factory method of your own, just requiring IServiceClient as input type, to make it a bit more readable. ;-)) (您可能希望将整个调用放在您自己的某个工厂方法中创建实例,只需要将IServiceClient作为输入类型,以使其更具可读性。;-))

In my tests I can just injected a mocked implementation of the service and test whether the right business methods were called and their results properly handled. 在我的测试中,我可以注入一个模拟的服务实现,并测试是否调用了正确的业务方法并正确处理了它们的结果。

    /// <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
    {
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM