2015-01-20 4 views
4

현재 종속성 주입을 사용하지 않는 코드로 작업하고 있으며 WCF 클라이언트를 통해 여러 서비스 호출을합니다.유닛 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; 
     } 
    } 
} 

제 목표는이 코드를 종속성 삽입을 사용하여 단위 테스트 할 수있게 만드는 것입니다. 나의 첫 번째 생각은 서비스 클라이언트의 인스턴스를 클래스 생성자에 전달하는 것이다. 그런 다음 유닛 테스트에서 실제 웹 서비스 요청을하지 않는 테스트 용 모의 클라이언트를 만들 수 있습니다.

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

그러나, 나는이이 질문의 정보를 기반으로, 원래의 행동에 영향을 미치는 방식으로 코드를 변경하는 것을 깨달았다 : Operation1에 대한 호출이 실패 할 경우, 원래의 코드에서 Reuse a client class in WCF after it is faulted

및 클라이언트가 오류 상태가되고 ServiceClient의 새 인스턴스가 만들어지고 Operation2가 계속 호출됩니다. 업데이트 된 코드에서 Operation1에 대한 호출이 실패하면 동일한 클라이언트가 Operation2를 호출하기 위해 다시 사용되지만 클라이언트가 오류 상태 인 경우이 호출이 실패합니다.

종속성 주입 패턴을 유지하면서 클라이언트의 새 인스턴스를 만들 수 있습니까? 문자열을 사용하여 클래스를 인스턴스화하는 데 리플렉션을 사용할 수 있지만 리플렉션이 올바른 방법이 아닌 것 같아요.

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

그런 MyClass에 당신은 단순히 예에게이 요구 될 때마다 얻을 공장을 사용합니다 :

+0

유일한 문제는 클라이언트를 만드는 것입니까? 내 경험에 따르면 생성 된 클래스의 메소드가 항상 가상이 아니기 때문에 생성 된 클라이언트 클래스가 아닌 비즈니스 인터페이스를 기반으로 조롱 된 클라이언트를 반환하려고합니다. 또한 실제 클라이언트는 사용 된 후에 처리해야합니다 (코드에서 보지 못했습니다). 이것은 비즈니스 인터페이스가 일반적으로 지원하지 않는 것입니다. –

답변

6

당신은 인스턴스 자체보다는 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>으로 충분해야합니다. 인스턴스 생성 로직이 복잡해지면 명시 적으로 구현 된 팩토리를 다시 고려해야합니다.

1

이전에는 서비스의 비즈니스 인터페이스를 기반으로 각 호출에 대해 ChannelFactory에서 새로운 연결을 만들고 각 호출 후에 해당 연결을 닫는 일반 클라이언트 (Unity를 사용하는 '차단'이 있음) 예외 또는 정상 응답이 리턴되었는지 여부에 따라 연결에 오류가 있음을 표시할지 여부를 결정합니다. (아래 참조)

이 클라이언트를 사용하는 내 실제 코드는 비즈니스 인터페이스를 구현하는 인스턴스를 요청하고이 일반 래퍼의 인스턴스를 가져옵니다. 리턴 된 인스턴스는 처리 될 필요가 없으며 예외가 리턴되었는지 여부에 따라 다르게 처리됩니다. 아래의 래퍼를 사용하여 서비스 클라이언트를 얻으려면 내 코드가 : var client = SoapClientInterceptorBehavior<T>.CreateInstance(new ChannelFactory<T>("*"))이며 대개 레지스트리에 숨겨져 있거나 생성자 인수로 전달됩니다. 따라서 귀하의 경우에 나는 var myClass = new MyClass(SoapClientInterceptorBehavior<IServiceClient>.CreateInstance(new ChannelFactory<IServiceClient>("*")));으로 끝날 것입니다. (아마도 여러분 자신의 일부 factory 메소드에서 인스턴스를 생성하기 위해 전체 호출을하고 싶습니다. 단지 입력 형식으로 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 
    { 
    } 
} 
관련 문제