Wednesday, September 27, 2017

Unit testing close to the UI

One of the reasons for moving the ViewModel out of the code-behind touted by MVVM evangelists is the ability to perform unit testing. Unfortunately there are costs associated with moving the code that outweigh the benefits. You have to replace RoutedCommands with RelayCommands and the binding of ModelView to View is abstruse. Here is a solution that shows how you can write unit tests for well written MVVM applications without those costs.

Start a new WPF project. I'm in Visual Studio 2015 working in C#.


We will create a simple page that adds two numbers and displays the result or an error. It looks like this...




It will be written with MVVM but the ViewModel will be in the page's code-behind. That means the binding will be intuitive and we can use simple RoutedCommands. Replace the contents of MainWindow.xaml with this...

<Window x:Class="UnitTestAdd.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:UnitTestAdd"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <StackPanel Orientation="Horizontal" Height="20">
        <TextBox Text="{Binding Addend1, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
        <TextBlock Text=" + "/>
        <TextBox Text="{Binding Addend2, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
        <TextBlock Text=" = "/>
        <TextBlock Text="{Binding Sum}" Background="{Binding BackgroundColor}"/>
    </StackPanel>
</Window>

Note that DataContext refers to Self. That allows us to put the ViewModel in the code behind. We will implement INotifyPropertyChanged as all MVVM designs have to. Replace the contents of MainWindow.xaml.cs with this...

using System;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Media;

namespace UnitTestAdd
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private string _Addend1;
        private string _Addend2;
        private string _Sum;
        private Brush _BackgroundColor;

        public String Addend1
        {
            get { return _Addend1; }
            set
            {
                if (_Addend1 != value)
                {
                    _Addend1 = value;
                    NotifyPropertyChanged("Addend1");
                    CalculateSum(_Addend1, _Addend2);
                }
            }
        }
        public String Addend2
        {
            get { return _Addend2; }
            set
            {
                if (_Addend2 != value)
                {
                    _Addend2 = value;
                    NotifyPropertyChanged("Addend2");
                    CalculateSum(_Addend1, _Addend2);
                }
            }
        }
        public String Sum
        {
            get { return _Sum; }
            set
            {
                if (_Sum != value)
                {
                    _Sum = value;
                    NotifyPropertyChanged("Sum");
                }
            }
        }
        public Brush BackgroundColor
        {
            get { return _BackgroundColor; }
            set
            {
                if (_BackgroundColor != value)
                {
                    _BackgroundColor = value;
                    NotifyPropertyChanged("BackgroundColor");
                }
            }
        }
        public MainWindow()
        {
            InitializeComponent();
        }

        private void CalculateSum(String Addend1, String Addend2)
        {
            try
            {
                Sum = (int.Parse(Addend1) + int.Parse(Addend2)).ToString();
                BackgroundColor = new SolidColorBrush(Colors.Transparent);
            }
            catch (Exception ex)
            {
                Sum = ex.Message;
                BackgroundColor = new SolidColorBrush(Colors.Red);
            }
        }
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }

    }
}

Whenever the user enters something into either of the TextBoxes we attempt to perform integer addition and display the result. If inputs are missing or invalid we display an error message instead.

Now let's add a unit test. The easiest way to do this is to add another project to the solution. Right click on the Solution in Solution Explorer and select Add New Project. Chose a Unit Test Project and call it "UnitTestAddTest".


You will need to add a reference to the project you are testing plus a few more. Make sure all the references shown below are in your unit test project.


Now write your unit test. Replace the content of UnitTest1.cs with this...

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using UnitTestAdd;

namespace UnitTestAddTest
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestAdd()
        {
            MainWindow mw = new UnitTestAdd.MainWindow();
            mw.Addend1 = "1";
            mw.Addend2 = "1";
            Assert.AreEqual("2", mw.Sum, "Can't even add up!");
        }
    }
}

Because we used MVVM, when the unit test changes properties in MainWindow it's just like the user typing. You can run the unit test by clicking on "Test" in the menu, then Windows and test Explorer. This displays the test explorer page. If you haven't built the solution, do so now.

Click on "Run All" and you should be rewarded with a green check mark.

Now deliberately break the application. Return to MainWindow.xaml.cs and replace the + with * to give you Sum = (int.Parse(Addend1) * int.Parse(Addend2)).ToString();

Run the unit test again and you will see a red X and an error message.


Message boxes and other popups work with this technique too. Undo the change you just made so the code reads like Sum = (int.Parse(Addend1) + int.Parse(Addend2)).ToString(); again and add a line below to display a message box like this...

Sum = (int.Parse(Addend1) + int.Parse(Addend2)).ToString();
BackgroundColor = new SolidColorBrush(Colors.Transparent);
MessageBox.Show("The answer is " + Sum);

When you run the program you see the answer displayed in a message box.


Now try running the unit test again. The message box popups up out of nowhere and the test waits for you to respond before continuing. Sometimes the message box pops up behind Visual Studio so keep an eye on your task bar.





1 comment: