单元测试WCF客户端

时间:2015-01-20 06:01:36

标签: c# wcf unit-testing

我正在使用当前不使用任何依赖项注入的代码,并通过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,但如果客户端处于故障状态,则此调用将失败。

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

2 个答案:

答案 0 :(得分:6)

你需要注入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<IServiceClient>就足够了。一旦实例创建逻辑变得更加复杂,就会重新考虑显式实现工厂。

答案 1 :(得分:1)

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

我使用此客户端的真实代码只是请求实现业务接口的实例,它将获得此通用包装器的实例。返回的实例不需要根据是否返回异常进行处理或处理。为了获得服务客户端(使用下面的包装器),我的代码执行:var client = SoapClientInterceptorBehavior<T>.CreateInstance(new ChannelFactory<T>("*")),它通常隐藏在注册表中或作为构造函数参数传入。所以在你的情况下,我最终会得到var myClass = new MyClass(SoapClientInterceptorBehavior<IServiceClient>.CreateInstance(new ChannelFactory<IServiceClient>("*")));(你可能想把整个调用放在你自己的一些工厂方法中创建实例,只需要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
    {
    }
}