Testing WCF Service Apps (Part 2 of 4)

Previous posts:
Part 0 of 4: Introduction
Part 1 of 4: Testing the Service
Shout it kick it on DotNetKicks.com

Testing the Client

So far, I outlined how to test your WCF service.  I simply took advantage or the WCF architecture and tested the service directly outside of the actual service harness.  Now I need to set my sights on the client.  This becomes a bit more difficult, but I wouldn’t say that it is necessarily hard.  I will start by giving a typical textbook example of hooking up to our service, and then I will tell you what is wrong with it.  I will continue by modifying the code to be more testable so that the service can be mocked. 

A Textbook Example

Most WCF tutorials and books have you start by adding a service reference to your service.  Doing this will generate a proxy client that you can use in your application.  My client application is a very simple data mining application.  You can give it the ingredient name and the application will return all recipes that include the ingredient of choice.  The first step is creating the service reference.  Right-click on the project and select "Add Service Reference".

AddServiceReference

Once the service has been added, I can use it in my code:

public class IngredientFinder
{
    public IEnumerable<RecipeData> GetRecipes(string ingredientName)
    {
        var recipeService = new RecipeBoxServiceClient();

        return from recipe in recipeService.AllRecipes()
               where recipe.ContainsIngredient(ingredientName)
               select recipe;
    }
}

This is all I need to get my app up and running. My main function just calls into this code and prints the results.  There is, however, one major flaw with this code: it is not testable!  This is because if I were to instantiate an instance of IngredientFinder, I would be required to hook up to a WCF service via the RecipeBoxServiceClient.  Technically I could write this code but I wouldn’t recommend it.  For one, it requires a lot of setup to harness the service.  Secondly, it drifts away from being a unit test and becomes more of a functional or integration test (more on this in part 4).  Thirdly, you can’t always assume that you control the service.  Assume, for instance, that you are writing an application that connects to a service like Twitter.  You certainly don’t want your unit tests hitting the only instance of the service: the live one.

We need to do something about this…

Making it Testable

If you were to inspect the RecipeBoxServiceClient class that was generated for you from the service specification, you would find one very important detail:  RecipeBoxServiceClient implements the auto-generated interface IRecipeBoxService.  Let us make a modification to the IngredientFinder to make it testable:

public class IngredientFinder
{
    private readonly IRecipeBoxService _recipeService;

    public IngredientFinder(IRecipeBoxService recipeService)
    {
        if(recipeService == null) throw new ArgumentNullException("recipeService");

        _recipeService = recipeService;
    }

    public IEnumerable<RecipeData> GetRecipes(string ingredientName)
    {
        return from recipe in _recipeService.AllRecipes()
               where recipe.ContainsIngredient(ingredientName)
               select recipe;
    }
}

This is a classic example of dependency injection.  The user of the class (in our case, the main method) is now responsible for defining the service to use.  This way, the IngredientFinder doesn’t need to know anything about the connection details.  In addition, I can write tests that mock out the service completely.

Writing Tests Against the Client

In my example, I am using Rhino Mocks, but you can use any mock/fake/stub framework/method you wish.

[TestFixture]
public class TestIngredientFinder
{
    private MockRepository _mocks;
    private IRecipeBoxService _mockService;
    private IngredientFinder _finder;

    [SetUp]
    public void SetUp()
    {
        _mocks = new MockRepository();
        _mockService = _mocks.CreateMock<IRecipeBoxService>();
        _finder = new IngredientFinder(_mockService);
    }

    [Test]
    public void Test_IngredientFinder_With_Cheese()
    {
        Expect.Call(_mockService.AllRecipes()).
            Return(new[] {Recipe("Mac&Cheese", "Macaroni", "Cheese")});
        _mocks.ReplayAll();        

        var recipes = _finder.GetRecipes("Cheese").ToArray();

        Assert.That(recipes.Length, Is.EqualTo(1));
        Assert.That(recipes[0].Title, Is.EqualTo("Mac&Cheese"));
    }

    [Test]
    public void Test_IngredientFinder_With_Two_Recipes_That_Have_Cheese()
    {
        Expect.Call(_mockService.AllRecipes()).Return(new[]{
                                                              Recipe("Mac&Cheese", "Macaroni", "Cheese"),
                                                              Recipe("Grilled Cheese", "Cheese", "Bread")
                                                          });
        _mocks.ReplayAll();

        var recipes = _finder.GetRecipes("cheese").ToArray();

        Assert.That(recipes.Length, Is.EqualTo(2));
        Assert.That(recipes[0].Title, Is.EqualTo("Mac&Cheese"));
        Assert.That(recipes[1].Title, Is.EqualTo("Grilled Cheese"));
    }

    [Test]
    public void Test_IngredientFinder_Finding_Nothing()
    {
        Expect.Call(_mockService.AllRecipes()).Return(new[]{
                                                              Recipe("Mac&Cheese", "Macaroni", "Cheese"),
                                                              Recipe("Grilled Cheese", "Cheese", "Bread")
                                                          });
        _mocks.ReplayAll();

        var recipes = _finder.GetRecipes("chicken").ToArray();

        Assert.That(recipes.Length, Is.EqualTo(0));
    }

    [Test, ExpectedException(typeof(ArgumentNullException))]
    public void Test_For_Null()
    {
        _finder.GetRecipes(null);
    }

    [Test, ExpectedException(typeof(ArgumentNullException))]
    public void Test_Constructor_With_Null_Service_Interface()
    {
        var junk = new IngredientFinder(null);
    }
}

With this test suite, I have full coverage on my IngredientFinder class and I never needed to instantiate the actual service. 

Next time

I will discuss how to test your client code with asynchronous service references.  It turns out that it is not as straight-forward as the synchronous approach (this post), so I will devote an entire post to the asynchronous case. (Part 3 of 4)

Leave a Reply