What Was I Thinking?

Follies & Foils of .NET Development
posts - 95 , comments - 352 , trackbacks - 0

Solved: Unit/Integration Testing of a WCF Service

I've got a solution to running and debugging unit tests for WCF hosted services that utilize the CMServiceHost.  Whether your generate your serivce proxy via WSDL (Add Service Reference) or Shared Contract (ClientFactory), the solution is similar.  This email will detail how to invoke a WCF Service Host for testing in both scenarios.

Testing a service client generated via "Add Service Reference"
I'll illustrate the solution using the following sample unit test.  Let's assume you've created a service proxy for the "TestService" service with Add Service Reference, and assigned it to the "TestSvc" namespace.   You're test looks like this:

   1:      [TestMethod]
   2:          public void Test_InvokeTestService()
   3:              {
   4:                  var client = new TestSvc.TestServiceClient();
   5:                  var pingResp = client.Ping();
   6:                  TestContext.WriteLine("Ping Response: {0}", pingResp);
   7:              } 

Let's assume this service is hosted by our ChannelDelivery.Host project and the above test lives int he ChannelDelivery.Test project.

   1. Start by getting latest of the _ReferencedAssemblies in the dev branch of TFS.
   2. In your Test Project, add a reference to the Platform.UnitTests_Utilities assembly (its available in the _ReferencedAssemblies\Framework folder in the Applications solutions).
   3. Decorate your unit test with the AspNetDevelopmentServer attribute.  This attribute takes two arguments; a unique host idenfiter, and the location of the web site to run.  Assuming  You're running this for the ChannelDelivery unit tests, you'd specify the arguments as follows: [AspNetDevelopmentServer("ChannelDeliveryHost", @"ChannelDelivery\ChannelDelivery.Host")]
   4.  In your unit test code, after your create your service proxy, and before you invoke the service operation, add a call to

     client.Endpoint.Address= WebTestHelper.GetRedirectedEndpointAddress("ChannelDeliveryHost", TestContext, client.Endpoint.Address);

Your code should now resemble:

      [TestMethod]
    [AspNetDevelopmentServer("ChannelDeliveryHost", @ "ChannelDelivery\ChannelDelivery.Host")]
            public void Test_InvokeTestService()
            {
                var client = new TestSvc.TestServiceClient();
               client.Endpoint.Address= WebTestHelper.GetRedirectedEndpointAddress("ChannelDeliveryHost", TestContext, client.Endpoint.Address);
                var pingResp = client.Ping();
                TestContext.WriteLine("Ping Response: {0}", pingResp);
            }

Testing a service client that shares service contract and uses ClientFactory.
I'll illustrate the solution using the following sample unit test.  Let's assume you've created a service proxy for the "EchoService" service using the ClientFactory.   You're test looks like this:

            [TestMethod]
            public void Test_ChannelDeliveryEcho()
            {
                using (var svc = ClientFactory.ProduceServiceClient<IEchoService>())
                {
                    TestContext.WriteLine("Echo Test Results:" + svc.Client.Echo("Echo!"));

                }      
            }

To enable integration testing of the EchoService:

   1. Start by getting latest of the _ReferencedAssemblies in the dev branch of TFS.
   2. Decorate your unit test with the AspNetDevelopmentServer attribute.  This attribute takes two arguments; a unique host identifier, and the location of the web site to run.  Assuming  You're running this for the ChannelDelivery unit tests, you'd specify the arguments as follows: [AspNetDevelopmentServer("ChannelDeliveryHost", @"ChannelDelivery\ChannelDelivery.Host")]
   3.  In your unit test code, after your create your service proxy, and before you invoke the service operation,  call the RedirectServiceUri method on the service proxy, passing a reference to the TestContext.

Your code should now resemble:

            [TestMethod]
            [AspNetDevelopmentServer("ChannelDeliveryHost",@"ChannelDelivery\ChannelDelivery.Host")]
            public void Test_ChannelDeliveryEcho()
            {
                using (var svc = ClientFactory.ProduceServiceClient<IEchoService>())
                {
                    svc.RedirectServiceUri(TestContext);
                    TestContext.WriteLine("Echo Test Results:" + svc.Client.Echo("Echo!"));
                }      
            }

Summary
When you debug or run this unit test, the AspNetDevelopmentServer (the one that runs when you press f5 on your web site), starts up and runs on a random port.   The WebTestHelper and RedirectServiceURI methods, uses the currently executing test context to determine the random port assignment, and determines the new service address based on the random port and current service uri.  The methods also ensure the new uri is valid by attempting to send a POST request to the uri.

You can now run your integration unit tests that rely on hosted wcf services.

 

The code for GetRedirectedEndpointAddress (aka GetRedirectedUriString) follows:

 

Code Snippet
  1.  
  2. public static string GetRedirectedUriString(object TestContext, string defaultUriStr)
  3. {
  4.     return GetRedirectedUriString(TestContext, defaultUriStr, null);
  5. }
  6.  
  7. public static string GetRedirectedUriString(object TestContext, string DefaultUriStr, string HostIdentifier)
  8. {
  9.     Uri hostUri = getHostUriFromTestContext(TestContext, HostIdentifier);
  10.     if (hostUri == null) return DefaultUriStr;
  11.     var defaultUri = new Uri(DefaultUriStr);
  12.     string newUriStr = hostUri.Scheme + "://" + hostUri.Authority + defaultUri.PathAndQuery;
  13.     newUriStr = EnsureValidUri(new Uri(newUriStr));
  14.     return newUriStr;
  15. }
  16.  
  17. private static Uri getHostUriFromTestContext(object context, string identifier)
  18. {
  19.     const string DevServerKeyName = "AspNetDevelopmentServer.";
  20.     const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
  21.     PropertyInfo info = context.GetType().GetProperty("Properties", bindingFlags);
  22.     if (info != null)
  23.     {
  24.         var contextProperties = info.GetValue(context, null) as Dictionary<string, object>;
  25.         if (contextProperties != null)
  26.         {
  27.             if (string.IsNullOrEmpty(identifier))
  28.             {
  29.                 var entry = contextProperties.FirstOrDefault(kvp => kvp.Key.StartsWith(DevServerKeyName));
  30.                 return entry.Value as Uri;
  31.             }
  32.             var uri = contextProperties[DevServerKeyName + identifier] as Uri;
  33.             return uri;
  34.         }
  35.     }
  36.     return null;
  37. }
  38.  
  39. private static string EnsureValidUri(Uri targetUri )
  40. {
  41.     var uri = targetUri.ToString();
  42.     for (int i = 0; i < targetUri.Segments.Length; i++ )
  43.     {
  44.         var response = WebTools.DoHttpPost(uri, null);
  45.         if (response.Contains("404"))
  46.         {
  47.             while (targetUri.Segments[i] == "/")
  48.                 i++;
  49.             if (i <= targetUri.Segments.Length)
  50.                 uri = uri.Replace(targetUri.Segments[i] , "");
  51.         }
  52.         else
  53.             return uri;
  54.     }
  55.     return targetUri.ToString();
  56.  
  57. }
  58. public static void WriteMessageToTestContext(object TestContext, string message)
  59. {
  60.     var mi = TestContext.GetType().GetMethod("WriteLine");
  61.     if (mi != null)
  62.     {
  63.         var parameters = new object[] {message, new object[] {}};
  64.         mi.Invoke(TestContext, parameters);
  65.     }
  66. }

Print | posted on Thursday, September 16, 2010 8:06 AM | Filed Under [ Visual Studio WCF ]

Feedback

No comments posted yet.
Post A Comment
Title:
Name:
Email:
Comment:
Verification:
 

Powered by: