Check out myAngular article series with live demos

Unit Testable Code with MVVM

Torgeir "Tor" Helgevold
- JavaScript Developer and Blogger
Published: Fri Jun 12 2015

Writing unit tests for UI code can be tricky. The code is usually full of dependencies to objects and user interactions that present challenges when attempting to write unit tests.

In this article I will describe how to reduce the impact of these dependencies by using the unit test friendly pattern Model-View-View-Model (MVVM).

Model-View-View-Model

As with all other Model-View-X patterns, the key idea behind MVVM is to design testable code with a clear separation between view code and application code. This separation is very difficult to achieve in the classic “code-behind” design that you often see in UI projects today, but with MVVM this becomes a lot easier.

This article will describe, through a simple sample project, how view models can be used to create fully testable UI code.

The example used in the article is a simple WPF temperature converter for converting between Fahrenheit and Celsius. WPF is extremely well suited for MVVM, but the pattern is in principle applicable to other UI technologies as well.

The most important component is the TemperatureConverterViewModel (Code listing 1) class which encapsulates all the application code for the converter. The idea is that no logic will exist outside of the view model, and the interface between the view and the view model is data binding. TemperatureConverterViewModel binds to the UI using change aware properties that are coded to detect changes resulting from either user action or code assignments.

Code listing 1

public class TemperatureConverterViewModel : INotifyPropertyChanged { private bool fahrenheitToCelsius; private bool celsiusToFahrenheit; private double from; private double to; private string fromHeading; private string toHeading; public TemperatureConverterViewModel() { FahrenheitToCelsius = true; } public double From { get { return from; } set { from = value; OnPropertyChanged("From"); ConvertTemperature(); } } public double To { get { return to; } set { to = value; OnPropertyChanged("To"); } } public bool FahrenheitToCelsius { get { return fahrenheitToCelsius; } set { fahrenheitToCelsius = value; OnPropertyChanged("Fahrenheit"); ConvertTemperature(); } } public bool CelsiusToFahrenheit { get { return celsiusToFahrenheit; } set { celsiusToFahrenheit = value; OnPropertyChanged("Celsius"); ConvertTemperature(); } } public string ToHeading { get { return toHeading; } set { toHeading = value; OnPropertyChanged("ToHeading"); } } public string FromHeading { get { return fromHeading; } set { fromHeading = value; OnPropertyChanged("FromHeading"); } } private void ConvertTemperature() { if (FahrenheitToCelsius) { To = (From - 32.0) * 5.0 / 9.0; FromHeading = "Fahrenheit"; ToHeading = "Celsius"; } else { To = (From * 9.0 / 5.0) + 32; FromHeading = "Celsius"; ToHeading = "Fahrenheit"; } } public event PropertyChangedEventHandler PropertyChanged; protected void OnPropertyChanged(string name) { PropertyChangedEventHandler handler = PropertyChanged; if (handler != null) { handler(this, new PropertyChangedEventArgs(name)); } } }

The WPF binding model is very powerful and extremely flexible, and as a result there is no need for boilerplate code in the code-behind to manage assignments and updates of UI controls. Instead it's all defined in the xaml (Code listing 2). As you can tell, there is only a single line of code in the code-behind (Code listing 3).

Code listing 2

<grid margin="5,0,0,0" x:name="main"> <grid.rowdefinitions> <rowdefinition height="20"></rowdefinition> <rowdefinition height="20"></rowdefinition> <rowdefinition height="50"></rowdefinition> </grid.rowdefinitions> <grid.columndefinitions> <columndefinition></columndefinition> <columndefinition></columndefinition> </grid.columndefinitions> <textblock fontweight="Bold" grid.row="0" grid.column="0" text="{Binding Path=FromHeading}"></textblock> <textblock fontweight="Bold" grid.row="0" grid.column="1" text="{Binding Path=ToHeading}"></textblock> <textbox grid.row="1" grid.column="0" text="{Binding Path=From,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></textbox> <textbox grid.row="1" grid.column="1" isenabled="False" text="{Binding Path=To,Mode=TwoWay,StringFormat=.0 }"></textbox> <stackpanel grid.row="2" grid.column="0" orientation="Vertical"> <radiobutton ischecked="{Binding Path=FahrenheitToCelsius,Mode=TwoWay}" content="Fahrenheit to Celsius"></radiobutton> <radiobutton ischecked="{Binding Path=CelsiusToFahrenheit,Mode=TwoWay}" content="Celsius to Fahrenheit"></radiobutton> </stackpanel> </grid>

Code listing 3

public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); main.DataContext = new TemperatureConverterViewModel(); } }

Since the view model contains all the application code, writing unit tests now becomes very easy (Code listing 4).

Code listing 4

[TestFixture] public class TemperatureConverterViewModelTest { [Test] public void Will_Convert_From_Fahrenheit_To_Celsius() { var temperatureConverterViewModel = new TemperatureConverterViewModel(); temperatureConverterViewModel.From = 95; temperatureConverterViewModel.FahrenheitToCelsius = true; Assert.AreEqual("35.0",temperatureConverterViewModel.To.ToString(".0")); Assert.AreEqual("Fahrenheit", temperatureConverterViewModel.FromHeading); Assert.AreEqual("Celsius", temperatureConverterViewModel.ToHeading); } [Test] public void Will_Convert_From_Celsius_Fahrenheit() { var temperatureConverterViewModel = new TemperatureConverterViewModel(); temperatureConverterViewModel.From = 35; temperatureConverterViewModel.CelsiusToFahrenheit = true; temperatureConverterViewModel.FahrenheitToCelsius = false; Assert.AreEqual("95.0", temperatureConverterViewModel.To.ToString(".0")); Assert.AreEqual("Celsius", temperatureConverterViewModel.FromHeading); Assert.AreEqual("Fahrenheit", temperatureConverterViewModel.ToHeading); } }

As you can tell from the two test cases, we are able to successfully test not only the conversions, but also that appropriate UI messages are displayed correctly.

If you liked this article, it to your friends.

I also invite you to follow me on twitter