簡體   English   中英

單元測試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;
        }
    }
}

我的目標是通過使用依賴注入使這個代碼單元可測試。 我的第一個想法是簡單地將服務客戶端的實例傳遞給類構造函數。 然后在我的單元測試中,我可以創建一個模擬客戶端用於測試目的,但不會向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
        }
    }
}

但是,我意識到這會以一種影響其原始行為的方式更改代碼,基於此問題的信息: 在WCF出現故障后重新使用WCF中的客戶端類

在原始代碼中,如果對Operation1的調用失敗並且客戶端處於故障狀態,則會創建一個新的ServiceClient實例,並且仍將調用Operation2。 在更新的代碼中,如果對Operation1的調用失敗,則重用相同的客戶端來調用Operation2,但如果客戶端處於故障狀態,則此調用將失敗。

是否可以在保持依賴注入模式的同時創建客戶端的新實例? 我意識到反射可以用來從字符串中實例化一個類,但我覺得反射不是解決這個問題的正確方法。

您需要注入工廠而不是實例本身:

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

然后在MyClass您只需使用factory來在每次需要時獲取實例:

// 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>應該足夠了。 一旦實例創建邏輯變得更復雜,那么將是重新考慮顯式實現工廠的時間。

我過去所做的是有一個通用客戶端(使用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
    {
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM