[英]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.