对 WCF 客户端进行单元测试

Unit Testing 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;
        }
    }
}

我的目标是通过使用依赖注入使这段代码可以进行单元测试。我的第一个想法是简单地将服务客户端的实例传递给 class 构造函数。然后在我的单元测试中,我可以创建一个模拟客户端用于测试目的,它不会向 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,但如果客户端处于故障状态,则此调用将失败。

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

您需要注入 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> 委托注入返回此类客户端的函数,这样您就可以避免创建额外的 class 和接口:

// 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> 应该足够了。一旦实例创建逻辑变得更加复杂,就需要重新考虑明确实现的工厂了。

我过去所做的是有一个通用客户端('interception' 使用 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
    {
    }
}