span.sup { vertical-align:text-top; }

Test Run

Introduction to WCF Testing

Dr. James McCaffrey

Code download available at:TestRun2008_07.exe(173 KB)

Contents

The System under Test
The Test Harness
Additional Considerations

Whether you are new to Windows® Communication Foundation (WCF) or have worked with it a bit, there are some testing techniques and principles that will make your WCF work easier. There are several ways to think about what WCF is—I tend to think of WCF services as a major extension of Web services. Like Web services, WCF services allow you to create distributed systems using a service-oriented architecture. However, WCF services provide much greater flexibility (such as choice of transport protocol) and additional features (such as transactions and security). WCF is much more than merely an extension of Web services, but if you are new to WCF, initially thinking about WCF services in this way is a reasonable approach.

Figure 1 shows a simple but representative WCF scenario. Here Internet Explorer® is acting as a client program and is accessing an ASP.NET Web application that accepts some text from the user and computes a crypto-hash of it. Behind the scenes, the ASP.NET Web app is calling a WCF service to actually perform the hashing computation. In this particular scenario, the WCF service is being hosted by IIS and being consumed by an ASP.NET Web app, but as I will explain shortly, WCF services can be hosted in several ways in addition to IIS and can be consumed by virtually any type of application or other service.

fig01.gif

Figure 1 Typical WCF Application Scenario

The most basic type of WCF service testing involves verifying the functional correctness of the service operations. One approach would be to manually test a WCF service through an application UI. But, although manual testing is necessary, using this approach to test the basic functionality of a WCF service would be time consuming, error prone, inefficient, and just plain boring.

A better approach would be to write test automation similar to the program shown running in Figure 2. The screenshot shows a console application test harness I wrote that feeds input text directly to the back-end WCF service, fetches the response message from the service, and determines a pass/fail test case result. The diagram in Figure 3 is a simplified view and summary of the relationships between the programs shown inFigures 1 and 2. In many cases, WCF services retrieve information from a back-end database, or retrieve information from Web services or WCF services, but I have not included those scenarios in Figure 3.

fig02.gif

Figure 2 Testing a WCF Service

fig03.gif

Figure 3 Simplified Relationships

Next, I'll explain the back-end WCF service so you'll understand exactly what is being tested, briefly discuss the ASP.NET Web application shown in Figure 1 which consumes the WCF service, and describe the test harness in detail. I'll conclude by touching upon some other WCF testing scenarios.

The System under Test

The system under test consists of a back-end WCF service and an ASP.NET Web application that uses the WCF service. WCF services are remarkably flexible. One of your key design decisions when creating a WCF service involves choosing a hosting mechanism for the service. There are four main options: using IIS, using a Windows® Service, self hosting, and using Windows Activation Service (WAS). You're probably familiar with the use of IIS and Windows. Self-hosting involves hosting WCF within a Microsoft® .NET Framework managed program such as a console application. WAS is a new process activation mechanism available in Windows Server® 2008 and in Windows Vista®. Each WCF hosting option has advantages and disadvantages depending upon your particular development scenario. For the sample WCF service in this column I decided to use IIS. This choice leverages IIS advantages such as built-in integrated management and monitoring, process recycling, idle shutdown, and message-based activation.

Creating a WCF service hosted by IIS is stunningly easy. I begin by firing up Visual Studio® 2008 on Windows Server 2003. Note that if you decide to develop a WCF service on a machine running Windows Server 2008 or Windows Vista, during development you will have to deal with issues related to their enhanced security features. However, I do not have space to describe them here.

Virtual Lab: Testing with LINQ

LINQ can dramatically simplify test automation when testing SQL-stored procedures. You can experiment with these LINQ testing techniques in our Virtual Lab. Everything is installed and ready to go, including the projects discussed in the column and a preconfigured SQL Server® database. Just start the lab and code along as you read.

Experiment in the virtual lab:

go.microsoft.com/fwlink/?LinkId=120461

Next, I select the File | New | Web Site option from the Visual Studio menu. Then I choose the WCF Service template (installed by default in Visual Studio 2008) from the New Web Site dialog and target the .NET Framework 3.5. For the Location field, I select HTTP and specify localhost/WCF/CryptoHashService. This approach creates a full Web Application and IIS virtual directory at C:\Inetpub\wwwroot\WCF\CryptoHashService on my development machine; instead I could have selected a location on my machine's file system and used the built-in Visual Studio Web Development Server.

I decided to use C# as my implementation language; however, WCF services can be implemented using Visual Basic® .NET, too. After clicking the OK button on the dialog box, Visual Studio creates a fully functional WCF service with two example operations named GetData and GetData­UsingDataContract. If you look at the Solution Explorer window, you'll see that Visual Studio generates four key files: IService.cs, Service.cs, Service.svc, and web.config. File IService.cs holds the interface definitions for WCF operations and File Service.cs holds the actual implementations of your operations. In this case, I decide to rename these two files as ICryptoHashService.cs and CryptoHashService.cs, respectively. Next, I load ICryptoHashService.cs into the Visual Studio code editor, delete the sample interface code, and replace it with this code:

[ServiceContract]
public interface ICryptoHashService
{
    [OperationContract]
    string GetCryptoHash(string s);
}

Here I have only a single operation, GetCryptoHash, but I could have added other operations. Notice that the [SeviceContract] and [OperationContract] attributes will be doing most of the actual code generation for me behind the scenes. Next, I edit the implementation file CryptoHashService.cs by adding a using statement reference to the System.Security.Cryptography namespace, and I write this code:

public class CryptoHashService : ICryptoHashService
{
    public string GetCryptoHash(string s)
    {
        byte[] ba = Encoding.Unicode.GetBytes(s);
        MD5CryptoServiceProvider sp = new MD5CryptoServiceProvider();
        byte[] hash = sp.ComputeHash(ba);
        string result = BitConverter.ToString(hash);
        return result;
    }
}

First, I change the name of my CryptoHashService class and the class from which it derives to match the name I use in my interface definition. In my GetCryptoHash method, I simply use the GetBytes method to convert the input argument to a byte array, instantiate an instance of the MD5CryptoServiceProvider class, and use the ComputeHash method to convert my byte array to a 16-byte MD5 crypto-hash. I convert the resulting hash from a byte array to a human-friendly string using the static BitConverter.ToString method, and then return that string. To keep my example short I have omitted the normal error checking you'd use in a production environment, such as checking my input argument, catching exceptions, and so on. Next, I edit the Service.svc file to reflect my naming changes from "Service" to "CryptoHashService":

<%@ ServiceHost Language="C#" Debug="true"   Service="CryptoHashService"
           CodeBehind=           "~/App_Code/CryptoHashService.cs" %>

I finished my name editing by updating two name references in web.config:

<system.serviceModel>
         <services>
         <service name="CryptoHashService"     behaviorConfiguration="Servic Behavior">
    <!-- Service Endpoints -->
    <endpoint address=""binding="wsHttpBinding"         contract="ICryptoHashService">

Notice the binding="wsHttpBinding" entry. A WCF binding is a collection of information that specifies how a WCF service communicates with clients including the transport protocol used by the service, the text encoding scheme used, and so on. You can use built-in bindings or create custom bindings. The wsHttpBinding is a pre-configured binding used by default when you create a WCF service hosted by IIS. Another part of the web.config file generated by Visual Studio is:

<endpoint address="mex" binding="mexHttpBinding"
    contract="IMetadataExchange"/>

This entry instructs my WCF service to expose metadata about itself so that client programs can probe the service to determine how to interact with it. At this point I can successfully build my WCF service by selecting Build | Build Solution from the main menu. I could have also hit the F5 shortcut key to both build my WCF service and get instructions for creating a client program. Because my service is being hosted by IIS, I don't need to start the service explicitly; the WCF service will be ready to accept incoming WCF messages as long as IIS is running.

Now let's walk through the creation of the ASP.NET Web application shown in Figure 1. I launch a new instance of Visual Studio and issue a File | New | Web Site command. On the New Web Site dialog, I select the ASP.NET Web Site template and select the .NET Framework 3.5 in the framework target dropdown control. I use Location HTTP and select C# as my language. I type localhost/WCF/UtilitiesAndTools in the location field to implicitly name my Web app. I could have specified a file system location on my development machine and used the development Web server instead of IIS. Next, I add some minimal UI code to my Web app.

The code in Figure 4 is the basic UI, without formatting details such font style and <hr/> tags. You can get the exact UI code, along with all the code presented in this article, from the code download that accompanies this column.

Figure 4 ASP.NET Web Application UI Code

<body>
   <form id="form1" runat="server">
      <div>
      <asp:Label runat="server" ID="Label"
         Text="Demo Utilities Featuring WCF Services" />
      <asp:Label runat="server" ID="Label2"
         Text="Enter text here:" />
      <asp:TextBox runat="server" ID="TextBox1"
         Height="100px" Width="320px" />
      <asp:Button runat="server" ID="Button1"
         Text="Get MD5 Crypto-Hash" onclick="Button1_Click"
         Width="150px" />
      <asp:Button runat="server" ID="Button2"
         Text="Get SHA1 Crypto-Hash"
         Width="150px" />
      <asp:Label runat="server" ID="Label3"
      Text="Crypto-Hash of your text (computed by WCF Service) is:" />
      <asp:TextBox runat="server" ID="TextBox2"
         Width="320px" />
      </div>
   </form>
</body> 

At this point, my ASP.NET Web application has no knowledge of my WCF service; however, WCF and ASP.NET technologies were designed to work together seamlessly. In the Solution Explorer window of my Web application project, I right-click on the project name and select Add Service Reference from the context menu. Notice this is a new option that complements the older Add Reference (typically used for DLL libraries and .NET namespaces) and Add Web Reference (typically used for ASP.NET Web services) options.

In the resulting Add Service Reference dialog, I type localhost/WCF/CryptoHashService/Service.svc and click on the Go button. The Add Service Reference tool will then scan the specified location for available services and display the services found; in this case the CryptoHashService WCF service. In the Namespace field on the dialog, I accept the simple, but not very descriptive, default name, ServiceReference1, then click the OK button. Visual Studio generates all the proxy code my application needs to connect to my WCF service. In particular, I get a class named CryptoHashServiceClient (that is, "Client" appended to the name of the WCF service), which allows me to communicate with the CryptoHashService service.

In design view, I double-click on the Button1 control to instruct Visual Studio to register the control's event handler. Then I add this code to the Button1_Click method:

try  
{
    string s = TextBox1.Text;
    ServiceReference1.CryptoHashServiceClient c = 
       new ServiceReference1.CryptoHasahServiceClient();
    string h = c.GetCryptoHash(s);
    TextBox2.Text = h;
}
catch(Exception ex)
{
    TextBox2.Text = ex.Message;
}

It's almost too easy. I grab the text in TextBox1, instantiate an instance of the autogenerated CryptoHashServiceClient class, call the object's GetCryptoHash method, and display the resulting MD5 (Message Digest version 5) crypto-hash in TextBox2. The user interface of the Web application in Figure 1 shows a button control that computes a SHA-1 (Secure Hash Algorithm 1) crypto-hash, but I did not implement that functionality to simulate a system under development. After building the Web application, a user can launch Internet Explorer and navigate to the app, enter some text, and get the MD5 crypto-hash of the text, computed by the back-end WCF service, as shown in Figure 1.

The Test Harness

Now let's take a look at the code for the simple test harness shown in Figure 2. In essence, the test harness program is just a WCF client that sends an input message to the WCF service under test and verifies a correct return message. I begin by launching a new instance of Visual Studio and creating a C# console application program named TestHarness. In the example shown in Figure 2, notice I set the location of my harness as a subdirectory of C:\Inetpub\wwwroot just to keep my harness code located near my system under test. However, I could have placed my harness anywhere, including on a separate test host machine. Figure 5 illustrates the overall structure of the test harness.

Figure 5 Test Harness Structure

using System;
using System.Collections.Generic;
using System.Text;
using System.ServiceModel;

namespace TestHarness
{
    class Program
    {
        static void Main(string[] args)
        {
        try
            {
                 // display startup messages
                 // set up test case data collection, testCases
                 // set up WCF binding object, wsb
                 // set up WCF address object, epa

                 CryptoHashServiceClient c =
                     new CryptoHashServiceClient(wsb, epa);

                foreach (TestCaseData tcd in testCases)
                {
                     // echo case ID, input, expected values
                     // call GetCryptoHash method, fetch return
                     // compare actual return with expected
                     // print pass/fail result
                }
                Console.WriteLine("\nDone");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Fatal error: " + ex.Message);
            }
        }
    }

    class TestCaseData
    {
        public readonly string caseID;
        public readonly string input;
        public readonly string expected;
        public TestCaseData(string caseID, string input, string expected)
        {
            this.caseID = caseID; this.input = input; this.expected =
              expected;
        }
    }
}

I begin coding my test harness by adding a using statement that points to the System.ServiceModel namespace. This namespace holds the core WCF service functionality. Next, I set up my test case data. In Figure 5 you can see that I create a simple TestCaseData class to hold test case ID, input, and expected value. In a production scenario you could add other fields too, including WCF service binding information, as I'll explain shortly. Now I embed my test case data directly into my harness by creating a generic List object and populating it with TestCaseData objects:

List<TestCaseData> testCases = new List<TestCaseData>();
testCases.Add(new TestCaseData("001", "Hello world",
    "E6-76-0D-55-5C-32-F6-6F-5E-15-93-31-DB-20-FD-8E"));
testCases.Add(new TestCaseData("002", "Goodbye world",
    "1A-05-3C-C0-4A-18-13-06-0E-AC-EA-BA-46-EC-CF-B1"));
testCases.Add(new TestCaseData("003", "",
    "D4-1D-8C-D9-8F-00-B2-04-E9-80-09-98-EC-F8-42-7E"));

I could have set up my test case data in an external store such as an XML file or SQL table. Here I am using three test cases, but in the real world I could have thousands. Identifying good test case inputs that thoroughly exercise the system under test, and determining expected results is the most difficult part of software testing.

Now, although you can write WCF client code from scratch, it's much easier to use the svcutil.exe command-line tool that ships with Visual Studio 2008 to generate proxy code and a WCF configuration file. In this case, I launch a Visual Studio command shell (which knows the location of svcutil.exe) and enter the command:

> svcutil.exe https://localhost/WCF/CryptoHashService/Service.svc

Svcutil.exe running with the location of a WCF service, and without any additional arguments, reads WCF metadata from the service and generates two files for me. The first file is CryptoHashService.cs (the name of the targeted WCF service followed by a .cs extension), which holds C# proxy code that will allow me to send messages to and receive messages from my WCF service. The second file is output.config, which holds the WCF binding information (transport protocol, time-out setting, text encoding, and so on) used by the targeted WCF service. After generating these two files, I right-click on my test harness project and add the CryptoHashService.cs file to my project. Alternatively, I could have copied and pasted the code directly into my test harness.

Now there are two ways to use the binding information in the output.config file. One way is to rename the file to app.config and then add the file to your client project. The second approach is to examine the data in the output.config file and then write code that programmatically assigns the values shown in output.config to a binding object. When writing a normal WCF client program, the app.config approach is usually a better choice than the programmatic approach. Because an app.config file is ordinary text and is read by the client program at run time, if the binding information of the associated WCF service changes, you can change the corresponding binding information of your client simply by changing your app.config file, without recompiling your client. However, in the special case of a WCF test harness client, it's sometimes better to specify binding information programmatically. The programmatic approach allows you to easily pass in binding information as part of your test case input. Here I use the programmatic approach. I instantiate a WSHttpBinding object:

WSHttpBinding wsb = new WSHttpBinding();

Now I can assign values to the Name property and various time properties of the binding object:

wsb.Name = "WSHttpBinding_ICryptoHashService";
wsb.CloseTimeout = TimeSpan.Parse("00:01:00");
wsb.OpenTimeout = TimeSpan.Parse("00:01:00");
wsb.ReceiveTimeout = TimeSpan.Parse("00:10:00");
wsb.SendTimeout = TimeSpan.Parse("00:01:00");

I determine these values by visually examining the attributes of the binding entry in the output.config file, which was generated by the svcutil.exe tool. In this case, I am using all the default values that were generated by Visual Studio when I created my WCF service. The remaining binding properties are given values similarly:

wsb.BypassProxyOnLocal = false;
wsb.TransactionFlow = false;
wsb.HostNameComparisonMode =
    System.ServiceModel.HostNameComparisonMode.StrongWildcard;
wsb.MaxBufferPoolSize = 524288;
wsb.MaxReceivedMessageSize = 65536;
wsb.MessageEncoding =
    System.ServiceModel.WSMessageEncoding.Text;
wsb.TextEncoding = System.Text.Encoding.UTF8;
wsb.UseDefaultWebProxy = true;
wsb.AllowCookies = false;

Now I can set up my proxy object:

string uri =
    "https://vte014.vte.local/WCF/CryptoHashService/Service.svc";
EndpointAddress epa = new EndpointAddress(uri);
CryptoHashServiceClient c =
    new CryptoHashServiceClient(wsb, epa);

The CryptoHashServiceClient class is defined inside the auto-generated CryptoHashService.cs file I created using svcutil.exe. That class constructor accepts a binding object (which I just set up programmatically) and an EndPointAddress object that points to the WCF service. Now that my client object is instantiated, I can iterate through each test case and exercise the WCF service under test.

In Figure 6 I simply send a pass/fail message to the console for each test case. In a production environment you would likely do more—track the total number of cases that pass and the number that fail, perhaps send an e-mail message programmatically if one or more test cases fail, save results to an external data store, and so on. If you are using Team Foundation Server, you can manage this test harness using the techniques in my Test Run column "Custom Test Automation with Team System" from the 2008 Launch issue (see msdn.micro­­soft.com/magazine/cc164248).

Figure 6 Display Status for Each Test Case

foreach (TestCaseData tcd in testCases)
{
    Console.WriteLine("Case ID  = " + tcd.caseID);
    Console.WriteLine("Input    = " + tcd.input);
    Console.WriteLine("Expected = " + tcd.expected);

    string actual = c.GetCryptoHash(tcd.input);
    Console.WriteLine("Actual   = " + actual);
    if (actual == tcd.expected)
        Console.WriteLine("* Pass *");
    else
        Console.WriteLine("** FAIL **");
}

Additional Considerations

The techniques I've presented this month provide a solid foundation for getting started with basic WCF service testing. However, there are many other aspects of WCF testing that I will cover in future columns. Most of these additional testing topics are made possible by the great flexibility of WCF. For example, WCF allows systems to use security at the transport level (using HTTPS, for instance) as well as at lower levels. Although WCF services can use HTTP, WCF allows systems to communicate using several other mechanisms including TCP and named pipes. WCF services can be hosted in IIS, as I've shown, but WCF services can also be hosted in other ways including via Windows services and self-hosted managed applications. WCF services can support multiple endpoints, each of which can have a different address, binding, and contract. WCF supports request-reply-style messaging and duplex-style messaging. All these WCF scenarios, and many others, have interesting implications for thorough testing.

The basic WCF functional testing scenario presented in this column represents just one part of thorough WCF testing. Because my dummy WCF crypto-hash service is so simple, the entire logic is contained in a single GetCryptoHash method. In more realistic scenarios you may have code that encapsulates business logic and separate code that encapsulates the service functionality. This approach allows you to test business logic and services separately, which can simplify your test effort.

When creating WCF services using Visual Studio Team System, you can take advantage of the built-in unit testing support if you use a test driven development philosophy. You can also use the WcfTestClient.exe test client utility which ships with Visual Studio 2008 to perform manual testing of your WCF services to complement the type of automated testing I've presented in this column (see the column written by my MSDN® Magazine colleague Juval Lowy at msdn.microsoft.com/magazine/cc163289). In addition to purely functional testing, you may also want to perform load testing using the integrated load-testing tools in Visual Studio.

Send your questions and comments for James to testrun@microsoft.com.

Dr. James McCaffrey works for Volt Information Sciences, Inc., where he manages technical training for software engineers working at Microsoft. He has worked on several Microsoft products including Internet Explorer and MSN Search. James is the author of .NET Test Automation Recipes. He can be reached at jmccaffrey@volt.com or v-jammc@microsoft.com.