Blendable MVVM: Dependency Injection and Unit Testing

Sunday, October 11, 2009 by Nigel Sampson

There are a couple of great reasons for using a UI separation pattern such as Model, View, ViewModel (or the more common MVP and MVC), the first being that with the application logic separated from the view logic the application becomes easier to maintain. Secondly it's usually easier for a designer and developer to work on the separate areas and integrate them at a later date (this actually helps enforce the separation). And thirdly by having the application logic away from the UI it becomes much easier to test.

While you can do UI testing with the Silverlight testing framework it becomes a hell of a lot easier when you have a nice separation. We're doing to be using a dependency injection framework (in this caseNinject) to build the view models and inject the services. This'll allow us to do interaction based unit testing on the view model using Rhino Mocks.

Overall our goal will be to refactor the existing ViewModel to break it's dependency on CocktailsService and increase it's testability. We'll create a Service Locator that will be an Application resource and serve as almost a "ViewModel factory" and then instead of directly creating our ViewModel in xaml we'll bind our Data Context to the Service Locator.

Our first step will be inverting the dependency the ViewModel has on the Cocktails Service, we'll extract an interface for the service and use constructor injection to pass the service to the ViewModel where it'll be stored, otherwise the view model remains the same.

public interface ICocktailService

{

    IEnumerable<Cocktail> GetCocktails();

    IEnumerable<Cocktail> GetCocktailsSimilarTo(Cocktail cocktail);

}

public CocktailsViewModel(ICocktailService cocktailService)

{

    this.cocktailService = cocktailService;

 

    AvailableCocktails = new ObservableCollection<Cocktail>();

    SimilarCocktails = new ObservableCollection<Cocktail>();

 

    GetSimilarCocktailsCommand = new DelegateCommand<Cocktail>(GetSimilarCocktails);

 

    AvailableCocktails.AddRange(cocktailService.GetCocktails());

}

We can now test that the ViewModel exposes the available Cocktails, passes the command parameter to the service and exposes the similar cocktails.

[TestClass]

public class CocktailsViewModelFixture

{

    private readonly List<Cocktail> sampleCocktails = new List<Cocktail>

        {

            new Cocktail

            {

                Id = 1,

                Ingredients = new string[] {},

                Name = "Test"

            }

        };

 

    [TestMethod]

    public void ConstuctionRetrievesAvailableCockstails()

    {

        var cocktailService = MockRepository.GenerateMock<ICocktailService>();

 

        cocktailService.Expect(c => c.GetCocktails()).Return(Enumerable.Empty<Cocktail>());

 

        new CocktailsViewModel(cocktailService);

 

        cocktailService.VerifyAllExpectations();

    }

 

    [TestMethod]

    public void ConstuctionExposesAvailableCockstails()

    {

        var cocktailService = MockRepository.GenerateStub<ICocktailService>();

 

        cocktailService.Stub(c => c.GetCocktails()).Return(sampleCocktails);

 

        var viewModel = new CocktailsViewModel(cocktailService);

 

        Assert.AreEqual(sampleCocktails.Count, viewModel.AvailableCocktails.Count);

    }

 

    [TestMethod]

    public void GetSimilarCocktailsForwardsCommandParameter()

    {

        var selectedCocktail = new Cocktail();

 

        var cocktailService = MockRepository.GenerateMock<ICocktailService>();

 

        cocktailService.Stub(c => c.GetCocktails()).Return(Enumerable.Empty<Cocktail>());

        cocktailService.Expect(c => c.GetCocktailsSimilarTo(selectedCocktail)).Return(Enumerable.Empty<Cocktail>());

 

        var viewModel = new CocktailsViewModel(cocktailService);

 

        viewModel.GetSimilarCocktailsCommand.Execute(selectedCocktail);

 

        cocktailService.VerifyAllExpectations();

    }

 

    [TestMethod]

    public void GetSimilarCocktailsExposesSimilarCocktails()

    {

        var selectedCocktail = new Cocktail();

 

        var cocktailService = MockRepository.GenerateMock<ICocktailService>();

 

        cocktailService.Stub(c => c.GetCocktails()).Return(Enumerable.Empty<Cocktail>());

        cocktailService.Stub(c => c.GetCocktailsSimilarTo(selectedCocktail)).Return(sampleCocktails);

 

        var viewModel = new CocktailsViewModel(cocktailService);

 

        viewModel.GetSimilarCocktailsCommand.Execute(selectedCocktail);

 

        Assert.AreEqual(sampleCocktails.Count, viewModel.SimilarCocktails.Count);

    }

}

Astute readers will notice that because our ViewModel doesn't have a parameterless constructor we can't create it directly in xaml. This is correct, instead we'll be delegating the construction of the ViewModel to a Ninject kernel. We'll create a ServiceLocator that will be an Application Resource (in App.xaml), it will create the Ninject kernel and register the appropriate dependencies.

public class ServiceLocator

{

    private readonly IKernel kernal;

 

    public ServiceLocator()

    {

        kernal = new StandardKernel(new CocktailModule());

    }

 

    public CocktailsViewModel CocktailsViewModel

    {

        get

        {

            return kernal.Get<CocktailsViewModel>();

        }

    }

}

public class CocktailModule : StandardModule

{

    public override void Load()

    {

        Bind<CocktailsViewModel>().ToSelf();

        Bind<ICocktailService>().To<CocktailService>();

    }

}

 

<Application.Resources>

    <cx:ServiceLocator x:Key="ServiceLocator"/>

</Application.Resources>

For simplicities sake I have a simple module that will register the dependencies we need.

Unfortunately there's no nice GUI to hook the ViewModel up anymore we have to write our own Binding expression, but it's not too bad. This binds the DataContext of the UserControl to the property "CocktailsViewModel" which is in turn created by the Ninject kernal, injecting the appropriate dependencies.

DataContext="{Binding Path=CocktailsViewModel,Source={StaticResource ServiceLocator}}"

We're now back to where we were before but now with a unit tested view model. Later on I'll do a post on integration testing the entire stack.

Shout it kick it on DotNetKicks.com

View Comments

Comments

Laurent Bugnion - Sunday, October 11, 2009
Interesting series. It makes me happy to see here some concepts that I implemented in the MVVM Light toolkit, a set of components that help implementing MVVM applications in WPF and Silverlight.
http://www.galasoft.ch/mvvm/getstarted

The concept of ServiceLocator for ViewModels (or, as I call it in my toolkit, "ViewModelLocator") was first mentioned, as far as I can say, by Jonas Follesoe in Silverlight. It is, as you mention here, the way that works best with Expression Blend.

In the toolkit, I don't use an IoC framework, because I didn't want to add a dependency, but it is very easy to modify the default application to integrate any IoC framework you prefer. This flexibility is a very big advantage of this implementation.

Looking forward to read more in this series.

Cheers,
Laurent

Nigel Sampson - Wednesday, October 21, 2009
Hi Laurent, Thanks for the comments, writing it has certainly made me think about the usefulness of "Blendability" and how it fits into the whole process.

I'll certainly have to look into MVVM Light, currently there's a lot of MVVM toolkits cropping up, each with their own conventions.

Cheers

Jepwyytn - Thursday, November 05, 2009
comment1

Piykxstu - Thursday, November 05, 2009
comment4

Rftungwv - Thursday, November 05, 2009
comment6

Yndxqixd - Friday, November 06, 2009
comment6

Skiyimpo - Friday, November 06, 2009
comment5

Lfmpfyys - Friday, November 06, 2009
comment6

Gqdthrse - Friday, November 06, 2009
comment1

Ysfvaqgn - Friday, November 06, 2009
comment2

Ysfvaqgn - Friday, November 06, 2009
comment2

Hpaffwbc - Saturday, November 07, 2009
comment5

Tmzkweoo - Saturday, November 07, 2009
comment1

Smfqfplu - Saturday, November 07, 2009
comment4

Awjxmmau - Saturday, November 07, 2009
comment3

Rjrchtth - Sunday, November 08, 2009
comment5

Kzqdnbsr - Sunday, November 08, 2009
comment5

Jlxyalyr - Sunday, November 08, 2009
comment2

Qynjnzvt - Sunday, November 08, 2009
comment1

Sxkotssh - Monday, November 09, 2009
comment4

Frdxzapo - Monday, November 09, 2009
comment2

Gvlmxigy - Monday, November 09, 2009
comment2

Mnadtwkn - Monday, November 09, 2009
comment1

Idbslxri - Tuesday, November 10, 2009
comment4

Rcmlyyaj - Tuesday, November 10, 2009
comment6

Rcjihmka - Tuesday, November 10, 2009
comment2

Groixhuy - Tuesday, November 10, 2009
comment1

Zzsbqqds - Tuesday, November 10, 2009
comment5

Tfvfyatr - Wednesday, November 11, 2009
comment6