Die kniffligen Fälle beim Testen – GUI

Unit Tests der GUI? Das automatisierte Testen der GUI (grafical user interface) einer Desktop Anwendung ist auf den ersten Blick eine größere Herausforderung. Doch mit ein paar simplen Tricks kommt man bereits sehr weit. Ich stelle Ihnen im folgenden einige einfache Möglichkeiten anhand des folgenden Beispiels vor.

Automatisierte Tests GUIDie Abbildung zeigt eine simple UI, die eine Liste von Adressen anzeigt. Im unteren Bereich befindet sich eine CheckBox, die den daneben stehenden Button beeinflusst. Das Beispiel ist konstruiert, um daran einige Techniken zu erklären. Ich habe es mit WPF erstellt. Die Techniken lassen sich problemlos auf WinForms übertragen.

MVVM – Model View ViewModel

Es kommt ein View nebst ViewModel und Data Binding zum Einsatz. Da die Werte in der Tabelle nicht verändert werden können, habe ich darauf verzichtet, im ViewModel Adresse das Interface INotifyPropertyChanged zu implementieren.

In den folgenden Listings sehen Sie den Xaml Code, das ViewModel Adresse sowie die Code Behind Datei.

<Window x:Class="ui.AdresslisteForm"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Adressliste" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="5*" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>

        <DataGrid x:Name="grid" AutoGenerateColumns="True" Grid.Row="0" />
        <StackPanel Grid.Row="1" Orientation="Horizontal" >
            <Button x:Name="button" Content="Do something" />
            <CheckBox x:Name="checkMe" Content="Check me..." Margin="14" />
        </StackPanel>
    </Grid>
</Window>
namespace ui
{
    public class Adresse
    {
        public string Strasse { get; set; }

        public string PLZ { get; set; }

        public string Ort { get; set; }
    }
}
using System;
using System.Collections.Generic;
using System.Windows;

namespace ui
{
    public partial class AdresslisteForm : Window
    {
        public AdresslisteForm() {
            InitializeComponent();
            button.Click += ButtonClickHandler;
            checkMe.Click += CheckBoxClickHandler;
        }

        public void SetAdressliste(IEnumerable<Adresse> adressliste) {
            grid.ItemsSource = adressliste;
        }

        public event Action ButtonClickEvent;

        internal void ButtonClickHandler(object sender, RoutedEventArgs e) {
            ButtonClickEvent();
        }

        internal void CheckBoxClickHandler(object sender, RoutedEventArgs e) {
            if (checkMe.IsChecked.Value) {
                button.IsEnabled = false;
            }
            else {
                button.IsEnabled = true;
            }
        }
    }
}

Den Dialog mit Daten anzeigen

Ein erster halb-automatisierter Unit Test sorgt zunächst einmal dafür, dass der Dialog mit Daten gefüllt und dann angezeigt wird. Im realen Projekt ersparen Sie sich mit Hilfe solcher Tests, dass Sie die Anwendung starten und an die entsprechende Stelle navigieren müssen. In komplexen Anwendungen bedeutet das eine große Zeitersparnis. Es müssen keine Beispieldaten in den Datenbanken hergestellt werden, etc. Programmatisch lassen sich solche Testfälle viel besser lösen. Insbesondere kann ein Dialog so bereits getestet werden, bevor er in die Anwendung integriert wird.

using System.ComponentModel;
using System.Threading;
using NUnit.Framework;

namespace ui.tests
{
    [TestFixture]
    public class AdresslisteTests
    {
        private AdresslisteForm sut;
        private BindingList<Adresse> adressliste;

        [SetUp]
        public void Setup() {
            sut = new AdresslisteForm();
            adressliste = new BindingList<Adresse> {
                new Adresse {PLZ = "12345", Ort = "Örtchen", Strasse = "Am Hang 5"},
                new Adresse {PLZ = "54321", Ort = "Pusemuckel", Strasse = "Weg 42"}
            };
        }

        [Test, Apartment(ApartmentState.STA), Explicit]
        public void Mehrere_Adressen_anzeigen() {
            sut.SetAdressliste(adressliste);
            sut.ShowDialog();
        }
    }
}

Der Unit Test trägt neben dem obligatorischen Test Attribute noch das Attribut Explicit, damit er nicht beim Ausführen aller Tests anspringt. Da der Test den Dialog mit ShowDialog öffnet, bleibt er solange am Bildschirm stehen, bis Sie ihn wieder schließen. Dieser Test ist eben nur halb-automatisiert: es werden automatisiert Testdaten vorbereitet und an den Dialog übergeben. Dann wird der Dialog geöffnet und bleibt zur Inaugenscheinnahme am Bildschirm stehen. Damit das technisch funktioniert, muss das Attribut Apartment(ApartmentState.STA) ergänzt werden, da WPF andernfalls meckert.

Testen, ob ein Button einen Event auslöst

Der folgende Unit Test überprüft, ob beim Betätigen des Buttons der erwartete Event ausgelöst wird. Auch dieser Test ist halb-automatisch. Im Test wird in der Arrange Phase eine Lambda Expression an den Event gebunden, die eine MessageBox anzeigt. Auf diese Weise kann ich den Test starten, auf den Button drücken und dann beobachten, ob die MessageBox angezeigt wird. Interessanter werden solche Tests, wenn beim Event Parameter mitgeliefert werden. Die können dann in der Nachricht der MessageBox angezeigt werden, um so zu prüfen, ob die erwarteten Werte geliefert werden.

[Test, Apartment(ApartmentState.STA), Explicit]
public void Button_Handler_löst_Ereignis_aus_MessageBox() {
    sut.ButtonClickEvent += () => MessageBox.Show("Hello from the ButtonClickEvent");
    sut.ShowDialog();
}

Solche halb-automatischen Tests sind eine Übergangslösung in Legacy Code Projekten. Statt mit F5 die Anwendung zu starten und komplett manuell zu testen, lassen sich auf diese Weise einige Abläufe automatisieren. Wirklich rund wird die Sache, wenn der Test vollständig automatisiert abläuft, also auch ein Assert enthält, um eine Annahme zu überprüfen.

Automatisierter Test

[Test, Apartment(ApartmentState.STA)]
public void Button_Handler_löst_Ereignis_aus() {
    var count = 0;
    sut.ButtonClickEvent += () => count++;

    sut.ButtonClickHandler(this, new RoutedEventArgs());

    Assert.That(count, Is.EqualTo(1));
}

Dieser Test läuft nun komplett automatisiert. An den Event des Buttons wird eine Lambda Expression gebunden, die einen Zähler inkrementiert. Nun soll der Button gedrückt werden, um dann zu prüfen, ob der Zähler auf 1 steht. Würde ich jetzt den Dialog mit ShowDialog öffnen, würde die Testausführung unterbrochen, bis der Dialog geschlossen wird. Folglich müsste der Dialog auf einem eigenen Thread geöffnet werden. Ferner bliebe dann noch die Frage offen, wie der Button programmatisch gedrückt werden kann. Ich verwende eine viel einfachere Lösung: ich rufe im Test die Methode auf, die in der UI am Button Click Event hängt. Das folgende Listing zeigt noch einmal den relevanten Ausschnitt aus der Code Behind Datei:

button.Click += ButtonClickHandler;

...

internal void ButtonClickHandler(object sender, RoutedEventArgs e) {
    ButtonClickEvent();
}

Um die Methode ButtonClickHandler im Test erreichen zu können, habe ich sie auf internal gesetzt. Details zum Thema Sichtbarkeit finden Sie in meinem früheren Blogbeitrag über die Sichtbarkeit in Tests.

Nach dem gleichen Schema kann ich nun automatisiert prüfen, ob beim Anhaken der CheckBox der Zustand des Buttons modifiziert wird.

[Test, Apartment(ApartmentState.STA)]
public void CheckBox_schaltet_Button() {
    Assert.That(sut.button.IsEnabled, Is.True);

    sut.checkMe.IsChecked = true;
    sut.CheckBoxClickHandler(this, new RoutedEventArgs());
    Assert.That(sut.button.IsEnabled, Is.False);

    sut.checkMe.IsChecked = false;
    sut.CheckBoxClickHandler(this, new RoutedEventArgs());
    Assert.That(sut.button.IsEnabled, Is.True);
}

Hier prüfe ich zunächst, ob der Button initial aktiviert ist. Anschließend simuliere ich im Test das Anklicken der CheckBox, in dem ich den Zustand IsChecked setze und dann den Handler CheckBoxClickHandler aufrufe, der am Click Event der CheckBox hängt. So kann ich dann anschließend prüfen, ob der Button wie erwartet modifiziert wurde.

Fazit

Selbstredend gehört solche Logik nicht in den View. „Richtig“ wäre es, ein ViewModel zu verwenden, welches diese Logik enthält. Das ViewModel ist dann einfach automatisiert testbar und beeinflusst den View mittels Data Binding. Im Falle von Legacy Code ist aber eben nicht alles „richtig“ gemacht worden, so dass die gezeigten Techniken dann helfen können, die Logik in der GUI durch Unit Tests automatisiert zu testen.

Schreibe einen Kommentar