6

I try to write System Tests in NUnit and I want to Invoke the UI using the UI Automation from ms.

For some reason my Invokes fail - I found some hints online that got me to a state where I can write a compiling test but my assertion fails.

Here is a compileable minimal example. My problem is the failing test in the example.

Application XAML

<Application x:Class="InvokeTest.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:InvokeTest"
             Startup="Application_Startup"/>

Application CS

using System.Windows;

namespace InvokeTest
{
    public partial class App : Application
    {
        private void Application_Startup(object sender, StartupEventArgs e)
        {
            var view = new MainWindow();
            var viewmodel = new MainWindowViewModel();
            view.DataContext = viewmodel;
            view.Show();
        }
    }
}

Window XAML

<Window x:Class="InvokeTest.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:InvokeTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
<TextBox x:Name="MyTextBox" x:FieldModifier="public" Text="{Binding TextProperty, UpdateSourceTrigger=PropertyChanged}" />
</Window>

Window CS

using NUnit.Framework;
using System.Diagnostics;
using System.Threading;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;

namespace InvokeTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

    public class MainWindowViewModel
    {
        string textfield;
        public string TextProperty
        {
            get { DebugLog("getter"); return textfield; }
            set { textfield = value; DebugLog("setter"); }
        }

        private void DebugLog(string function)
        {
            Debug.WriteLine(ToString() + " " + nameof(TextProperty) + " " + function + " was called. value: '" + textfield ?? "<null>" + "'");
        }

        [TestFixture, Apartment(ApartmentState.STA)]
        public class WPFTest
        {
            MainWindow view;
            MainWindowViewModel viewmodel;

            [SetUp]
            public void SetUp()
            {
                view = new MainWindow();
                viewmodel = new MainWindowViewModel();
                view.DataContext = viewmodel;
            }

            [Test]
            public void SetTextBox_NoAutomation()
            {
                string expected = "I want to set this";
                view.MyTextBox.Text = expected;
                Assert.AreEqual(expected, viewmodel.TextProperty);
                /*
                Test Name:  SetTextBox_NoAutomation
                Test Outcome:   Failed
                Result Message: 
                Expected: "I want to set this"
                But was:  null
                */
            }

            [Test]
            public void SetTextBox_UIAutomation()
            {
                string expected = "I want to set this";
                SetValue(view.MyTextBox, expected);
                Assert.AreEqual(expected, viewmodel.TextProperty);
                /*
                Test Name:  SetTextBox_UIAutomation
                Test Outcome:   Failed
                Result Message: 
                Expected: "I want to set this"
                But was:  null
                */
            }
            private static void SetValue(TextBox textbox, string value)
            {
                TextBoxAutomationPeer peer = new TextBoxAutomationPeer(textbox);
                IValueProvider provider = peer.GetPattern(PatternInterface.Value) as IValueProvider;
                provider.SetValue(value);
            }
        }
    }
}

EDIT #1: @Nkosi pointed out that there was a binding failure in my xaml
EDIT #2: Added a bit of boiler to enable manual testing and also added a usecase that shows behaviour without uiautomation. That is just a sidenote, uiautomation is the core of this question.

Johannes
  • 6,490
  • 10
  • 59
  • 108
  • 2
    Hopefully you know this already, but unit tests aren't really designed to test UI. It can probably be "hacked" in to working, but you will end up fighting the system as you go. UI testing is typically done as part of Coded UI Tests, and scripted integration testing. – Bradley Uffner Oct 06 '16 at 14:03
  • @BradleyUffner yeah i know :) It is actually so common that you can not google ui autoamtion without hitting on Coded UI Tests. I do think Coded UI Tests are superior but I wanted to dive into the uiautomation a bit. – Johannes Oct 06 '16 at 16:07

2 Answers2

2

Actually you can call the TextBox.Text Property.

view.MyTextBox.Text = expected;

In your view you are also binding to Text property on your view model while the view model in your test has a MyTextBox property. One or the other needs to be updated to match.

public class MainWindowViewModel
{
    public string Text { get; set; }
}

[TestFixture, Apartment(ApartmentState.STA)]
public class WPFTest
{
    [Test]
    public void SetTextBox()
    {
        //Arrange
        var expected = "I want to set this";

        var view = new MainWindow();
        var viewmodel = new MainWindowViewModel();
        view.DataContext = viewmodel;

        //Act
        view.MyTextBox.Text = expected;

        //Assert
        Assert.AreEqual(expected, viewmodel.Text);
    }
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • I filed the binding failure (I think :) ) . Your suggestion does not work on my machine - can you run it? – Johannes Oct 06 '16 at 14:27
  • other than the assertion? no. – Johannes Oct 07 '16 at 08:44
  • Put some breakpoints and step through the test during debug. put one on the viewmodel property set and see if it gets hit when you set the textbox. also look into making the viewmodel inherit from `INotifyPropertyChanged` – Nkosi Oct 07 '16 at 09:19
  • `INotifyPropertyChanged` is the other direction, when the viewmodel changes. I edit my minimal example to support manual testing so you can see that the gui does what it needs to do. – Johannes Oct 07 '16 at 09:46
0

I need to show the window. I assume it is to get the message pump running.

If someone can give Details on this I will set that answer as the accepted answer.

using NUnit.Framework;
using System.Diagnostics;
using System.Threading;
using System.Windows;
using System.Windows.Automation.Peers;
using System.Windows.Automation.Provider;
using System.Windows.Controls;

namespace InvokeTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }

    public class MainWindowViewModel
    {
        string textfield;
        public string TextProperty
        {
            get { DebugLog("getter"); return textfield; }
            set { textfield = value; DebugLog("setter"); }
        }

        private void DebugLog(string function)
        {
            Debug.WriteLine(ToString() + " " + nameof(TextProperty) + " " + function + " was called. value: '" + textfield ?? "<null>" + "'");
        }

        [TestFixture, Apartment(ApartmentState.STA)]
        public class WPFTest
        {
            MainWindow view;
            MainWindowViewModel viewmodel;

            [SetUp]
            public void SetUp()
            {
                view = new MainWindow();
                viewmodel = new MainWindowViewModel();
                view.DataContext = viewmodel;
                view.Show();
            }

            [TearDown]
            public void TearDown()
            {
                view.Close();
                view.DataContext = null;
                view = null;
                viewmodel = null;
            }

            [Test]
            public void SetTextBox_NoAutomation()
            {
                string expected = "I want to set this";
                view.MyTextBox.Text = expected;
                Assert.AreEqual(expected, viewmodel.TextProperty);
            }

            [Test]
            public void SetTextBox_UIAutomation()
            {
                string expected = "I want to set this";
                SetValue(view.MyTextBox, expected);
                Assert.AreEqual(expected, viewmodel.TextProperty);
            }

            private void SetValue(TextBox textbox, string value)
            {
                TextBoxAutomationPeer peer = new TextBoxAutomationPeer(textbox);
                IValueProvider provider = peer.GetPattern(PatternInterface.Value) as IValueProvider;
                provider.SetValue(value);
            }
        }
    }
}
Johannes
  • 6,490
  • 10
  • 59
  • 108
  • 1
    I think the original version that the binding is not AttachToContext. [The binding will most likely be resolved for the first time when the UpdateLayout method is called for the first time, if the DataContext is already set.](http://stackoverflow.com/questions/13875537/when-are-data-bindings-applied) The second version the view.Show will trigger UpdateLayout event. – zzczzc004 Oct 15 '16 at 01:56