Friday, September 29, 2017

Unit Testing page with a modal dialog

Writing a unit test for a page that displays a modal dialog requires a little extra work in the code being tested. The problem is that as soon as the dialog box is shown, control passes to it and the unit test hangs. You can create the dialog box, but you must not show it.

Let's create WPF project with a MainWindow that is simply a huge button. Clicking the button opens a Popup window as a modal dialog. The Popup has a text box that the user can enter text into and a text block that shows the lower case version of that text. Our unit test will ensure the text is correctly lowercased.

In Visual Studio (I'm using 2015), start a new WPF application and call it PageTransition.


MainWindow will just be a big button with a RoutedCommand. We already know how to unit test routed commands so I won't go into the details. The XAML looks like this.

<Window x:Class="PageTransition.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:PageTransition"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <RoutedCommand x:Key="OpenCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource OpenCommand}" Executed="OpenCommand_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <Button Content="Open" Command="{StaticResource OpenCommand}"/>
    </Grid>
</Window>

MainWindow.xaml.cs is also very simple. Note I made the handle to the popup (p) a class public property. The unit test requires this as we will see later.

using System;
using System.Linq;
using System.Windows;
using System.Windows.Input;

namespace PageTransition
{
    public partial class MainWindow : Window
    {
        public Window p = null;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            p = new Popup();
            p.Show();
        }
    }
}

Now add a window to the project and call it Popup.


The XAML for Popup looks like this. It simply contains a text box, text block, and a close button.

<Window x:Class="PageTransition.Popup"
        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:PageTransition"
        mc:Ignorable="d"
        Title="Popup" Height="300" Width="300"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Window.Resources>
        <RoutedCommand x:Key="CloseCommand"/>
    </Window.Resources>
    <Window.CommandBindings>
        <CommandBinding Command="{StaticResource CloseCommand}" Executed="CloseCommand_Executed"/>
    </Window.CommandBindings>
    <Grid>
        <StackPanel Orientation="Horizontal" Height="20">
            <TextBox Text="{Binding SomeText, UpdateSourceTrigger=PropertyChanged}" Width="100"/>
            <TextBlock Text="{Binding LowerText}" Width="100"/>
            <Button Content="Close" Command="{StaticResource CloseCommand}"/>
        </StackPanel>
    </Grid>
</Window>

Popup.xaml.cs is also very simple.

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

namespace PageTransition
{
    public partial class Popup : Window, INotifyPropertyChanged
    {
        private String _SomeText = "";
        private String _LowerText = "";
        public String SomeText
        {
            get { return _SomeText; }
            set { if (_SomeText != value)
                {
                    _SomeText = value;
                    LowerText = _SomeText.ToLower();
                    NotifyPropertyChanged("SomeText");
                }
            }
        }

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

        public Popup()
        {
            InitializeComponent();
        }

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

        private void CloseCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            this.Close();
        }
    }
}

If you run the solution now you can click on the big button to see the popup, then enter some text and make sure it is displayed in lower case. Then you can click the close button.



The unit test has to start MainWindow, simulate a click of the [Open] button, get a reference to the Popup window, simulate the user entering some text, verify the text is displayed correctly, and simulate a click on the [Close] button.

Add a new unit test project to the solution and call it PageTransitionTest. 


Now add references so the project's reference list looks like this.


The Code in UnitTest1.cs looks like this. The line "Popup p = (Popup)mw.p" shows why we save a reference to the popup in the public property p of MainWindow.

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

namespace PageTransitionTest
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethod1()
        {
            MainWindow mw = new MainWindow();
            RoutedCommand oc = (RoutedCommand)mw.Resources["OpenCommand"];
            oc.Execute(null, mw);

            Popup p = (Popup)mw.p;
            p.SomeText = "ABC";
            Assert.AreEqual("abc", p.LowerText, "Didn't lower case");

            RoutedCommand cc = (RoutedCommand)p.Resources["CloseCommand"];
            cc.Execute(null, p);
        }
    }
}

If you run the unit test it will succeed. However, if you go back to the MainWindow.OpenCommand_Executed method and change p.Show() to p.ShowDialog() you will find the test hangs because the Popup is now modal. We don't actually need to show the dialog box at all for the unit test. We need to way to know that we are in a unit test and prevent the ShowDialog from executing.

Add the following function to  MainWindow.xaml.cs.

private bool IsInUnitTest()
{
    return AppDomain.CurrentDomain.GetAssemblies().Any((x) => x.FullName.ToLower().Contains("testplatform"));
}

Now change p.ShowDialog() to if (!IsInUnitTest()) p.ShowDialog();

OpenCommand_Executed now looks like this.

private void OpenCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
    p = new Popup();
    if (!IsInUnitTest()) p.ShowDialog();
}

So when opening a dialog box we need to save the window object somewhere that the unit test can find it. Furthermore, if we are opening a modal dialog box we need to not show it if we are running as part of a unit test.

If you run the application you will still see the modal dialog box. If you run the unit test you will not see it but the test will still complete successfully.


No comments:

Post a Comment