Friday, September 29, 2017

Writing a unit test with a RoutedCommand

Continuing from yesterday's post, I want to show you how to write a unit test against a WPF page that uses a RoutedCommand. We will create a page that adds two integers when the user clicks a button. That button will be disabled until the user enters two valid integer addends, and will display their sum when the user clicks the button.

The unit test will validate that the button is disabled when one of the addends is invalid, and also that the correct sum is displayed.

Start a new Visual Studio WPF C# solution. I'm using VS  2015.


The MainPage.xaml will define a command and some controls. It also sets the page's DataContext to the code behind.

<Window x:Class="AddWithCommand.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:AddWithCommand"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <RoutedCommand x:Key="AddCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource AddCommand}" CanExecute="AddCommand_CanExecute" Executed="AddCommand_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <StackPanel Orientation="Horizontal" Height="20">
            <TextBox Text="{Binding Addend1, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
            <TextBlock Text=" + "/>
            <TextBox Text="{Binding Addend2, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
            <Button Content=" = " Command="{StaticResource AddCommand}"/>
            <TextBlock Text="{Binding Sum}"/>
        </StackPanel>
    </Grid>
</Window>

The code behind is fairly simple. Replace MainWindow.xaml.cs with this.

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Input;

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

        public MainWindow()
        {
            InitializeComponent();
        }

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

        public String Addend1
        {
            get { return _Addend1; }
            set
            {
                if (_Addend1 != value)
                {
                    _Addend1 = value;
                    NotifyPropertyChanged("Addend1");
                }
            }
        }

        public String Addend2
        {
            get { return _Addend2; }
            set
            {
                if (_Addend2 != value)
                {
                    _Addend2 = value;
                    NotifyPropertyChanged("Addend2");
                }
            }
        }
        public String Sum
        {
            get { return _Sum; }
            set
            {
                if (_Sum != value)
                {
                    _Sum = value;
                    NotifyPropertyChanged("Sum");
                }
            }
        }

        private void CalculateSum(String Addend1, String Addend2)
        {
            try
            {
                Sum = (int.Parse(Addend1) + int.Parse(Addend2)).ToString();
            }
            catch (Exception ex)
            {
                Sum = ex.Message;
            }
        }

        private void AddCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            int t;
            e.CanExecute = (int.TryParse(Addend1, out t) && int.TryParse(Addend2, out t));
        }

        private void AddCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            CalculateSum(Addend1, Addend2);
        }
    }

}

If you run the solution now you will see a page that can add two integers. Yay Computers!



Now let's write our unit test. Add a new unit test project to the solution and call it "AddWithCommandTest".



We will start by adding some references until our reference list looks like this...



Now in the AddWithCommandTest project change UnitTest1.cs to look like this.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using AddWithCommand;
using System.Windows.Input;

namespace AddWithCommandTest
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestAdd()
        {
            MainWindow mw = new AddWithCommand.MainWindow();
            RoutedCommand rc = (RoutedCommand)mw.Resources["AddCommand"];

            mw.Addend1 = "1";
            mw.Addend2 = "X";   // Command should be non-executable
            Assert.IsFalse(rc.CanExecute(null, mw), "AddCommand is executable when addends are invalid");

            mw.Addend2 = "2";   // Command should be executable
            Assert.IsTrue(rc.CanExecute(null, mw), "AddCommand is not executable when addends are valid");

            rc.Execute(null, mw);
            Assert.AreEqual("3", mw.Sum, "Can't even add up!");
        }
    }
}

This defines a test class called TestAdd. It instantiates our test page (which calls the constructor and InitializeComponent) but does not display the page. We can now access everything on the page. We get a reference to the AddCommand and start testing.

Our first test deliberately sets the second addend to an invalid value and verifies the command cannot be executed. The two arguments to CanExecute are as minimal as possible. If your CanExecute logic depends on more information in these arguments you will have to work harder to make the call.

The second test puts a valid value in the second addend and verifies the command can now be executed.

The third test executes the command and ensures the Sum property contains the expected value.

Note that I used the MainWindow reference as the second parameter in each of the calls. Technically they should be called with a reference to the button, but my function doesn't care and I didn't have a reference to the button within easy reach. You cannot use null for this argument because the function will not be called and there will be no error message.

To run your unit test, click on Test in the menu, then Windows -> Test Explorer. Now click "Run All" to see the result. Feel free to break the code in AddWithCommand to see how the test reacts.


No comments:

Post a Comment