Brian Genisio's House of Bilz

  Home  |   Contact  |   Syndication    |   Login
  62 Posts | 0 Stories | 118 Comments | 0 Trackbacks

News

Locations of visitors to this page

Archives

Post Categories

Who am I?

Previous Posts:
Part 0 of 4: Introduction
Part 1 of 4: Testing the Service
Part 2 of 4: Testing the Client

Shout it

Testing Asynchronous Clients

Up to this point, we have tested the service and we have tested the client -- both in isolation.  We have written unit tests and our code  has good coverage.  Unfortunately, my clients are not always synchronous.  In Silverlight client, for instance, the framework will not permit you to make synchronous service requests.  As it turns out, writing tests for asynchronous service clients is not straight-forward.  Thankfully, there are some hacks that you can take advantage of to write effective asynchronous tests.

Generating Asynchronous Service Clients

When you generate your service reference in Visual Studio, the advanced options allow you to specify an asynchronous proxy. 

Async

In a textbook example, your is a bit more complicated but it is still pretty easy to follow.  Instead of calling AllRecipes (as in the previous post), the class hooks the AllRecipesCompleted event and calls AllRecipesAsync().  When the service returns the results, the event is fired and the results are processed.

public class IngredientFinder
{
    public event EventHandler<IngredentFinderCompleteArgs> ProcessingComplete;
    private string _ingredientName;

    public void FindRecipes(string ingredientName)
    {
        var recipeService = new RecipeBoxServiceClient();

        _ingredientName = ingredientName;
        recipeService.AllRecipesCompleted += AllRecipes_Completed;
        recipeService.AllRecipesAsync();
    }

    void AllRecipes_Completed(object sender, AllRecipesCompletedEventArgs e)
    {
        var recipes = from recipe in e.Result
                      where recipe.ContainsIngredient(_ingredientName)
                      select recipe;

        if (ProcessingComplete != null)
            ProcessingComplete(this, new IngredentFinderCompleteArgs { Recipes = recipes });
    }
}

Making it Testable

Just like in part 2, the code works great but it is not at all testable.  IngredientFinder is tightly coupled to the RecipeBoxServiceClient.  In a unit testing environment, you cannot rely on WCF to host the service.  Unlike part 2, it is not as simple as replacing the concrete service with IRecipeBoxService.  The interface that is generated looks like this:

public interface IRecipeBoxService
{
    RecipeData[] AllRecipes();
    IAsyncResult BeginAllRecipes(AsyncCallback callback, object asyncState);
    RecipeData[] EndAllRecipes(IAsyncResult result);
}

I have no idea why Microsoft did this, but the interface does not include the interface that the concrete class implements.  Neither the AllRecipesCompleted event or the AllRecipesAsync method are found in the interface!  It includes the begin/end calls but those are some messy methods to use.  I don't want to require my IngredientFinder class to be required to use messy methods just to make my code testable.  This is where my hack comes in.  It takes advantage of the fact that the RecipeBoxServiceClient is a partial class:

public interface IRecipeBoxServiceAsync : IRecipeBoxService
{
    void AllRecipesAsync();
    event System.EventHandler<AllRecipesCompletedEventArgs> AllRecipesCompleted;
}

public partial class RecipeBoxServiceClient : IRecipeBoxServiceAsync
{}

So what did I do here?  I created a new, asynchronous interface that includes the IRecipeBoxService interface and also includes the event and async method that is implemented in the concrete class.  After that, I tell the class to implement the asynchronous interface via the partial keyword.  I don't have to write any implementation code because it has already been done for me -- the interface simply didn't include it.

Now that this is in place, we can modify the class slightly to pass the interface in via dependency injection:

public class IngredientFinder
{
    public event EventHandler<IngredentFinderCompleteArgs> ProcessingComplete;

    private readonly IRecipeBoxServiceAsync _recipeService;
    private string _ingredientName;

    public IngredientFinder(IRecipeBoxServiceAsync service)
    {
        if (service == null) throw new ArgumentNullException("service");

        _recipeService = service;
    }

    public void FindRecipes(string ingredientName)
    {
        if (string.IsNullOrEmpty(ingredientName)) throw new ArgumentNullException("ingredientName");

        _ingredientName = ingredientName;
        _recipeService.AllRecipesCompleted += AllRecipes_Completed;
        _recipeService.AllRecipesAsync();
    }

    void AllRecipes_Completed(object sender, AllRecipesCompletedEventArgs e)
    {
        var recipes = from recipe in e.Result
                      where recipe.ContainsIngredient(_ingredientName)
                      select recipe;

        if (ProcessingComplete != null)
            ProcessingComplete(this, new IngredentFinderCompleteArgs { Recipes = recipes });
    }
}

The Tests

The tests are also more complex.  The test needs to simulate an asynchronous service and fire events.  I am using Moq to mock out the service.

[TestFixture]
public class TestIngredientFinder
{
    private Mock<IRecipeBoxServiceAsync> _mockService;
    private IngredientFinder _finder;

    [SetUp]
    public void SetUp()
    {
        _mockService = new Mock<IRecipeBoxServiceAsync>(MockBehavior.Strict);
        _finder = new IngredientFinder(_mockService.Object);
    }

    private RecipeData Recipe(string recipeName, params string[] ingredientNames)
    {
        var result = new RecipeData {Title = recipeName};
        
        var quantities = new List<QuantityData>();
        foreach (string ingredientName in ingredientNames)
            quantities.Add(new QuantityData{Ingredient = new IngredientData{ Name = ingredientName}});

        result.Quantities = quantities.ToArray();
        return result;
    }

    private static AllRecipesCompletedEventArgs RecipeCompletedArgs(params RecipeData[] results)
    {
        return new AllRecipesCompletedEventArgs(new object[] {results}, null, false, null);
    }

    private static MockedEvent<AllRecipesCompletedEventArgs> RegisterCompletedHandler(Mock<IRecipeBoxServiceAsync> mockService)
    {
        var serviceCompletedHandler = mockService.CreateEventHandler<AllRecipesCompletedEventArgs>();
        mockService.Object.AllRecipesCompleted += serviceCompletedHandler;
        return serviceCompletedHandler;
    }

    [Test]
    public void Test_IngredientFinder_With_One_Recipe_That_Has_Cheese()
    {
        var serviceCompletedHandler = RegisterCompletedHandler(_mockService);
        _mockService.Expect(service => service.AllRecipesAsync());

        IEnumerable<RecipeData> recipes = null;
        _finder.ProcessingComplete += (sender, args) => recipes = args.Recipes;
        
        _finder.FindRecipes("Cheese");
        serviceCompletedHandler.Raise(RecipeCompletedArgs(Recipe("Mac&Cheese", "Cheese", "Macaroni")));

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

    [Test]
    public void Test_IngredientFinder_With_Two_Recipes_That_Have_Cheese()
    {
        var serviceCompletedHandler = RegisterCompletedHandler(_mockService);
        _mockService.Expect(service => service.AllRecipesAsync());

        IEnumerable<RecipeData> recipes = null;
        _finder.ProcessingComplete += (sender, args) => recipes = args.Recipes;

        _finder.FindRecipes("Cheese");
        serviceCompletedHandler.Raise(RecipeCompletedArgs(
                                          Recipe("Mac&Cheese", "Macaroni", "Cheese"),
                                          Recipe("Grilled Cheese", "Cheese", "Bread")));
        
        Assert.That(recipes.Count(), Is.EqualTo(2));
        Assert.That(recipes.ToList()[0].Title, Is.EqualTo("Mac&Cheese"));
        Assert.That(recipes.ToList()[1].Title, Is.EqualTo("Grilled Cheese"));
    }

    [Test]
    public void Test_IngredientFinder_Finding_Nothing()
    {
        var serviceCompletedHandler = RegisterCompletedHandler(_mockService);
        _mockService.Expect(service => service.AllRecipesAsync());

        IEnumerable<RecipeData> recipes = null;
        _finder.ProcessingComplete += (sender, args) => recipes = args.Recipes;

        _finder.FindRecipes("chicken");
        serviceCompletedHandler.Raise(RecipeCompletedArgs(
                                          Recipe("Mac&Cheese", "Macaroni", "Cheese"),
                                          Recipe("Grilled Cheese", "Cheese", "Bread")));

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

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

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

Next Time

In the last post of this series, I will discuss functional testing.  How do you test the round-trip functionality of your application?  I will re-visit the synchronous client and show some tricks that allow you to test all points of your application without requiring the WCF infrastructure to be running. (part 4 of 4)

posted on Friday, December 19, 2008 10:34 PM