Test-driven Development (TDD) in MVVM-Apps

ViewModels testgetrieben entwickeln
Kommentare

Einer der zentralen Vorteile des Model-View-ViewModel-Patterns (MVVM) ist es, dass sich ViewModels automatisiert testen lassen. Das ViewModel ist unabhängig vom UI und enthält nur die UI-Logik; eine Tatsache, die es zu einem idealen Kandidaten für Unit-Tests macht.

Doch um ein testbares ViewModel zu erhalten, muss ein Entwickler ein paar Punkte beachten, insbesondere beim Laden von Daten oder beim Anzeigen von Dialogen. Worauf es ankommt und wie ViewModels testgetrieben entwickelt werden, zeigt dieser Artikel. Dabei werden das Unit-Testing-Framework xUnit, die Mocking Library Moq und das Dependency-Injection-Framework Autofac eingesetzt.

„Code ohne Tests ist schlechter Code“

„Ich habe genug zu tun, da hab‘ ich doch nicht noch Zeit, um Unit-Tests zu schreiben“. Diese Aussage hört man nur allzu oft von Entwicklern, insbesondere von jenen, die noch nie in einem testgetriebenen Projekt mitgearbeitet haben. Von Michael Feathers, Autor des erfolgreichen Buchs „Working with Legacy Code“, stammt folgendes Zitat: „Code ohne Tests ist schlechter Code“. Der Grund ist folgender: Wenn es Tests gibt, kann ein Entwickler das Verhalten des Codes sehr schnell und verifizierbar ändern, da er nach dem Ändern des Codes die Unit-Tests laufen lässt, um zu sehen, ob er nicht aus Versehen bestehende Logik zerstört hat. Ohne Tests kann der Entwickler nach dem Ändern des Codes niemals wissen, ob der Code jetzt besser oder schlechter ist.

In diesem Artikel wird nur „guter Code“ geschrieben. Nach einem Blick auf Unit-Tests, Test-driven Development und das xUnit-Framework zeigt der Artikel, wie testbare ViewModels geschrieben werden und wie sie sich testen lassen.

Unit-Tests

Ein Unit-Test ist ein automatisierter Test, der bekanntlich ein Stück produktiven Code testet. Ein Unit-Test erfüllt dabei verschiedene Eigenschaften, die unter dem Akronym F.I.R.S.T zusammengefasst sind. Unit-Tests sind schnell (Fast), unabhängig voneinander (Independent), wiederholbar in einer beliebigen Umgebung (Repeatable), selbstvalidierend (Self-validating) und werden zeitlich mit oder sogar vor dem produktiven Code geschrieben (Timeley).

Damit diese Eigenschaften eines Unit-Tests erfüllt sind, muss der produktive Code ein entsprechendes Design erfüllen. Beispielsweise muss es möglich sein, den Unit-Test in einer beliebigen Umgebung zu wiederholen (Repeatable): im Zug, zuhause, im Büro oder sogar auf dem Mond ohne Internet. Das bedeutet, dass beispielsweise ein Datenbankzugriff nicht im produktiven Code enthalten sein darf; er muss mit einem Interface abstrahiert werden. Dann wird im produktiven Code auf dieses Interface programmiert und eben nicht auf eine konkrete Implementierung, die auf die Datenbank zugreift. Im Unit-Test kommt dann eine Testimplementierung des Interface zum Einsatz, ein so genanntes Mock-Objekt. Dieses Mock-Objekt hat die Aufgabe, den Datenbankzugriff für den Test zu simulieren. Näheres dazu später beim Datenzugriff aus einem ViewModel.

Das S im F.I.R.S.T-Akronym sagt, dass ein Unit-Test auch selbstvalidierend sein muss. Das bedeutet, dass ein Unit-Test entweder rot oder grün sein muss, also keine manuellen Schritte enthalten darf. Muss der Entwickler beispielsweise nach dem Test manuell prüfen, ob eine bestimmte Datei erstellt wurde, dann ist es kein Unit-Test mehr, da er sich nicht selbst validiert.

So viel zu den Regeln. Sind die Unit-Tests für den eigenen produktiven Code geschrieben, ergeben sich einige Vorteile:

  • Es lassen sich Änderungen durchführen, ohne dass existierende Logik zerstört wird.
  • Gute Tests ergeben auch eine gute Dokumentation.
  • Das Schreiben von Unit-Tests kann das Design des produktiven Codes positiv beeinflussen.
  • Das Schreiben von Unit-Tests erfordert, dass man bereits vor der Implementierung stärker über das Problem nachdenkt.

Doch läuft die Anwendung fehlerfrei und stabil, wenn alle Unit-Tests grün sind? Natürlich nicht, es braucht auch noch Integrationstests.

Unit-Test vs. Integrationstest

Unit-Tests allein sind kein Heilbringer. Viele grüne Unit-Tests bedeuten noch nicht, dass eine Anwendung danach keine Fehler mehr hat. Neben Unit-Tests, die nur ein Stück Code isoliert betrachten, gibt es viele andere Testarten. Eine sehr wichtige stellen die Integrationstests dar. Beim Integrationstest gibt es keine Mock-Objekte wie beim Unit-Test. Es wird beispielsweise auf eine Datenbank zugegriffen, es wird der richtige Web Service aufgerufen etc. Das heißt, beim Integrationstest geht es darum, wie die einzelnen getesteten Einheiten (Units) dann tatsächlich zusammenspielen.

Zum Schreiben von Integrationstests lässt sich auch ein Unit-Testing-Framework wie xUnit einsetzen. Dabei werden im Test dann eben keine Mock-Objekte, sondern die richtigen Objekte verwendet – beispielsweise für den Datenbankzugriff.

Test-driven Development

Test-driven Development (TDD) ist eine Vorgehensweise zum Schreiben von produktivem Code, die sich Unit-Testing zu Nutze macht. Dabei ist ein zentrales Prinzip, dass der Unit-Test vor und eben nicht mit oder nach dem produktiven Code geschrieben wird. Beim Einsatz von TDD befindet sich der Entwickler in einem ständig Kreislauf der drei Phasen Red/Green/Refactor (Abb. 1), die in dieser Reihenfolge immer wieder durchlaufen werden:

Abb. 1: Test-driven Development

Abb. 1: Test-driven Development

  1. Red: Es wird ein Unit-Test geschrieben, der fehlschlägt, da der produktive Code noch nicht implementiert wurde.
  2. Green: Der produktive Code wird erstellt/angepasst. Und zwar lediglich so, damit der Unit-Test grün wird.
  3. Refactor: Der produktive Code wird überarbeitet und strukturiert. Alle Unit-Tests müssen danach weiterhin grün sein. Ist der Entwickler zufrieden mit der Struktur, geht es weiter mit Schritt 1, indem der nächste Unit-Test geschrieben wird.

Test-driven Development hat neben den Vorteilen des klassischen Unit-Testings weitere Vorteile:

  • Da der Test vor dem produktiven Code geschrieben wird, hat der produktive Code auf jeden Fall ein testbares Design.
  • Das Schreiben des Tests vor dem produktiven Code erfordert eine genaue Analyse der Anforderungen.
  • Die eigene Logik kann bereits fertiggestellt werden, auch wenn beispielsweise der dazu benötigte Datenbankserver noch nicht läuft.

Insbesondere der letzte Punkt ist spannend: In größeren Teams können Entwickler ihren Teil bereits fertigstellen, auch wenn andere Teile der Software noch fehlen. Beispielsweise kann ein fehlender Datenbankzugriff in einem Unit-Test mit einem Mock-Objekt simuliert werden. Somit kann die Logik isoliert fertiggestellt werden, auch wenn der Datenbankzugriff noch nicht vorhanden ist. Es braucht dazu lediglich das entsprechende Interface, das später implementiert wird.

Spikes in TDD

Immer mit einem roten Unit-Test zu starten, ist in manchen Fällen etwas schwierig. Insbesondere, wenn sich ein Entwickler nicht sicher ist, ob der eingeschlagene Weg für ein größeres Feature auch funktionieren wird – dann ist es bei TDD natürlich unsinnig, zuerst mit einem Test zu starten. In einem solchen Fall wird ein so genannter Spike erstellt. Ein Spike ist ein Durchstich und ein Experiment; er stellt sicher, dass der eingeschlagene Weg funktioniert. Der in einem Spike geschriebene Code sollte nach Sicherstellung der Funktion laut Theorie wieder verworfen werden. Anschließend sollte es wieder TTD üblich nach Red/Green/Refactor weitergehen. In der Praxis sieht es meist so aus, dass erfahrene Entwickler Spikes schreiben, die danach auch zu 100 Prozent testbar sind, womit sie manchmal den klassischen TDD-Zyklus umgehen.

Das Testing-Framework xUnit

Es gibt viele interessante Unit-Testing-Frameworks. Mit Visual Studio wird das von Microsoft bereitgestellte MSTest installiert. MSTest ist mittlerweile – insbesondere im .NET-Core-Zeitalter – etwas in die Jahre gekommen. Microsoft arbeitet aktuell an einer Version 2 von MSTest, die allerdings noch nicht fertiggestellt ist.

Neben MSTest ist das aus der Java-Welt von JUnit auf .NET portierte NUnit sehr beliebt. NUnit wurde mittlerweile komplett neu geschrieben, um die Vorteile von .NET auszunutzen. Einer der Entwickler von NUnit hat sich dazu entschlossen, xUnit zu erstellen, da ihm sowohl NUnit als auch MSTest zu schwergewichtig waren. xUnit ist heute ebenfalls ein sehr beliebtes Framework und derzeit auch die erste Wahl des Autors. Einige Gründe dafür sind:

  • xUnit ist sehr schnell.
  • xUnit benötigt im Gegensatz zu MSTest kein spezifisches Testprojekt, stattdessen reicht eine einfache Klassenbibliothek aus.
  • xUnit und die für die Testausführung benötigten Klassen lassen sich einfach via NuGet installieren.
  • xUnit ist sehr leichtgewichtig: Die Testklassen selbst benötigen keine Attribute, sondern nur die Testmethoden.
  • Testmethoden lassen sich einfach parametrisieren.

Hinweis

In diesem Artikel wird auf xUnit gesetzt, aber wenn Sie MSTest oder NUnit nutzen, ist das auch in Ordnung. Wichtig ist allein die Tatsache, dass Sie überhaupt Unit-Tests schreiben, um wartbaren Code zu erhalten. Welches Unit-Testing-Framework Sie dafür einsetzen, ist zweitrangig.

Ein Testprojekt mit xUnit anlegen

Um mit xUnit ein Testprojekt anzulegen, wird eine Klassenbibliothek erstellt und die beiden NuGet-Packages „xUnit“ und „xUnit.runner.visualstudio“ hinzugefügt (Abb. 2).

Abb. 2: Die NuGet-Packages „xUnit“ und „xUnit.runner.visualstudio“ für ein Testprojekt mit xUnit

Abb. 2: Die NuGet-Packages „xUnit“ und „xUnit.runner.visualstudio“ für ein Testprojekt mit xUnit

Wurden die NuGet-Packages hinzugefügt, wird noch eine Referenz auf das Projekt hinzugefügt, das die zu testenden Klassen enthält. In diesem Artikel wird eine Referenz auf ein kleines WPF-Projekt namens FriendStorage.UI hinzugefügt, das eine MainViewModel-Klasse enthält.

Zum Testprojekt wird eine neue Klasse mit dem Namen MainViewModelTests hinzugefügt, in der die Testmethoden für diese MainViewModel-Klasse untergebracht werden.
Der erste Fall, der getestet werden soll, ist das Laden von Friend-Objekten. Somit wird eine ShouldLoadFriends-Methode hinzugefügt. Damit diese als Testmethode erkannt wird, wird sie mit dem Fact-Attribut aus dem Namespace XUnit markiert (Listing 1).

public class MainViewModelTests
{
  [Fact]
  public void ShouldLoadFriends()
  {
  }
}

Auch wenn die Methode noch leer ist, lässt sie sich bereits ausführen. Dazu wird in Visual Studio über das Hauptmenü TEST | WINDOWS | TEST EXPLORER der Test-Explorer aufgerufen. Nach einem Klick auf RUN ALL wird die Testmethode ausgeführt und als grün im Test-Explorer angezeigt (Abb. 3).

Abb. 3: Die Testmethode wird im Test-Explorer angezeigt

Abb. 3: Die Testmethode wird im Test-Explorer angezeigt

Bei einem Blick auf den Test-Explorer fällt auf, dass der Testmethode der voll qualifizierte Klassenname vorangestellt wird. Das lässt sich anpassen, indem zum Testprojekt eine Konfigurationsdatei mit dem Namen xUnit.runner.json hinzugefügt wird. Diese Datei muss mit ins Ausgabeverzeichnis kopiert werden, um den Test-Runner von xUnit anzupassen. Mit dem folgenden Code werden im Test-Explorer die Testmethoden ohne den voll qualifizierten Klassennamen angezeigt:

{
  "methodDisplay": "method"
}

Testbare ViewModels schreiben

Ein eifriger Entwickler hat in der MainViewModel-Klasse bereits etwas Code in der Load-Methode hinzugefügt, um Friend-Objekte mithilfe einer FriendDataService-Klasse zu laden und in einer ObservableCollection abzuspeichern (Listing 2).

public class MainViewModel:ViewModelBase
{
  public MainViewModel()
  {
    Friends = new ObservableCollection();
  }

  public void Load()
  {
    var dataService = new FriendDataService();
    var friends = dataService.GetFriends();

    foreach (var friend in friends)
    {
      Friends.Add(friend);
    }
  }

  public ObservableCollection Friends { get; private set; }
}

Genau diese Load-Methode der MainViewModel-Klasse soll jetzt in der Testmethode ShouldLoadFriends getestet werden. Allerdings ist in Listing 2 ein Problem zu erkennen. Die Load-Methode instanziert einen FriendDataService. Dieser FriendDataService könnte auf eine Datenbank, auf einen Web Service oder auf etwas anderes zugreifen. Das bedeutet, dass mit diesem FriendDataService ein Unit-Test nicht mehr in einer beliebigen Umgebung wiederholbar und es somit eben kein Unit-Test wäre. Und damit stellt sich die Frage, wie sich testbare ViewModels entwickeln lassen. Die Antwort ist recht banal: ViewModels werden testbar, indem Abhängigkeiten abstrahiert und ausgelagert werden. Diese Antwort gilt nicht nur für ViewModels, sondern auch für jede andere Klasse, die testbar sein soll. Im Fall des MainViewModels aus Listing 2 stellt der FriendDataService eine Abhängigkeit dar, die das Testen der Load-Methode nicht erlaubt. Woher soll ein Entwickler im Unit-Test wissen, wie viele Friend-Objekte dieser FriendDataService zurückgibt? Wie soll das Ganze in einer beliebigen Umgebung auch ohne Datenzugriff getestet werden können? Es gibt nur eine Lösung: Der Datenzugriff muss aus der ViewModel-Klasse abstrahiert werden.

Den Datenzugriff abstrahieren

Um den Datenzugriff aus der MainViewModel-Klasse zu abstrahieren, wird ein IFriendDataProvider-Interface eingeführt, das eine Methode LoadFriends wie folgt dargestellt definiert:

public interface IFriendDataProvider
{
  IEnumerable LoadFriends();
}

Das MainViewModel selbst wird angepasst. Im Konstruktor wird ein IFriendDataProvider-Objekt entgegengenommen und in der Instanzvariablen _dataProvider gespeichert. Anstelle der FriendDataAccess-Klasse wird in der Load-Methode des MainViewModels das IFriendDataProvider-Objekt verwendet (Listing 3).

public class MainViewModel:ViewModelBase
{
  private IFriendDataProvider _dataProvider;

  public MainViewModel(IFriendDataProvider dataProvider)
  {
    _dataProvider = dataProvider;
    Friends = new ObservableCollection();
  }

  public void Load()
  {
    var friends = _dataProvider.LoadFriends();

    foreach (var friend in friends)
    {
      Friends.Add(friend);
    }
  }

  public ObservableCollection Friends { get; private set; }
}

Mit dem Verwenden des IFriendDataProvider-Interfaces ist die Abhängigkeit zur FriendDataAccess-Klasse aus dem MainViewModel verschwunden. Die Load-Methode lässt sich jetzt einfach testen, indem der IFriendDataProvider gemockt wird.

Den Datenzugriff von Hand „mocken“

Um die Load-Methode des MainViewModels zu testen, wird im Testprojekt eine neue Klasse namens FriendDataProviderMock hinzugefügt, die das Interface IFriendDataProvider implementiert (Listing 4).

public class FriendDataProviderMock : IFriendDataProvider
{
  public IEnumerable LoadFriends()
  {
    yield return new Friend { Id = 1, FirstName = "Thomas" };
    yield return new Friend { Id = 2, FirstName = "Julia" };
  }
}

Mit der FriendDataProviderMock-Klasse lässt sich in der ShouldLoadFriends-Methode eine neue MainViewModel-Instanz erstellen (Listing 5). Anschließend wird auf dem MainViewModel die Load-Methode aufgerufen und mit der Assert-Klasse angenommen, dass die Friends Property des MainViewModels genau zwei Friend-Instanzen enthält. Das sind die zwei Instanzen, die vom FriendDataProviderMock-Objekt aus Listing 4 zurückgegeben werden.

public class MainViewModelTests
{
  [Fact]
  public void ShouldLoadFriends()
  {
    var viewModel = new MainViewModel(new FriendDataProviderMock());

    viewModel.Load();

    Assert.Equal(2, viewModel.Friends.Count);
  }
}

Wird der Test ausgeführt, ist er grün. Die händische Implementierung von IFriendDataProvider für den Test kann natürlich etwas aufwändig werden, wenn es viele Interfaces und viele Tests gibt. In einem solchen Fall lohnt sich der Einsatz einer Mocking Library wie beispielsweise Moq.

Moq einsetzen

In Listing 4 wurde händisch eine FriendDataProviderMock-Klasse für den Test erstellt. Diese Klasse kann sich ein Entwickler auch sparen, indem er die Moq Library einsetzt. Dazu einfach das NuGet-Package „Moq“ zum Testprojekt hinzufügen und wie in Listing 6 eine Instanz der generischen Mock-Klasse erzeugen. Als generischer Typ-Parameter wird dabei das Interface IFriendDataProvider angegeben. Auf der Mock-Instanz wird mit der Setup-Methode die LoadFriends-Methode des IFriendDataProvider aufgesetzt. Es wird eine Liste mit zwei Friend-Objekten als Return-Wert festgelegt. Hinter den Kulissen erzeugt die Mock-Klasse einen dynamischen Proxy für das IFriendDataProvider-Interface. Dieser dynamische Proxy wird mithilfe des Castle-Frameworks erstellt. Über die Object Property der Mock-Instanz lässt sich der dynamische Proxy abgreifen, der in Listing 6 eine IFriendDataProvider-Instanz darstellt – und diese lässt sich jetzt wunderbar zum Testen des MainViewModels verwenden.

public void ShouldLoadFriends()
{
  var dataProviderMock = new Mock();

  dataProviderMock.Setup(dp => dp.LoadFriends())
    .Returns(new List
    {
      new Friend {Id = 1, FirstName = "Thomas"},
      new Friend {Id = 1, FirstName = "Julia"}
    });
  IFriendDataProvider dataProvider = dataProviderMock.Object;
  var viewModel = new MainViewModel(dataProvider);

  viewModel.Load();

  Assert.Equal(2, viewModel.Friends.Count);
}

Dependency Injection mit Autofac

Nachdem die Load-Methode des MainViewModels fertiggestellt und getestet ist, wird eine IFriendDataProvider-Implementierung erstellt, die zur Laufzeit der Anwendung genutzt wird. Sie greift intern in der LoadFriends-Methode beispielsweise auf den FriendDataService zu, der vorher direkt im MainViewModel verwendet wurde (Listing 7).

public class FriendDataProvider : IFriendDataProvider
{
  public IEnumerable LoadFriends()
  {
    var dataService = new FriendDataService();
    return dataService.GetFriends();
  }
}

Ist der FriendDataProvider fertig, wird noch der Konstruktor des MainWindow angepasst, damit dieser ein MainViewModel entgegennimmt und es im DataContext speichert. Findet das Loaded-Event auf dem MainWindow statt, wird auf dem MainViewModel die Load-Methode aufgerufen (Listing 8).

public partial class MainWindow : Window
{
  private readonly MainViewModel _viewModel;

  public MainWindow(MainViewModel viewModel)
  {
    _viewModel = viewModel;
    InitializeComponent();
    this.Loaded += MainWindow_Loaded;
    DataContext = _viewModel;
  }

  private void MainWindow_Loaded(object sender, RoutedEventArgs e)
  {
    _viewModel.Load();
  }
}

Die WPF kann das MainWindow jetzt nicht mehr selbst instanziieren, da es keinen parameterlosen Konstruktor gibt. Daher wird in der Datei App.xaml die StartupUri Property entfernt und in der Codebehind-Datei die OnStartup-Methode überschrieben. Darin wird eine MainWindow-Instanz erstellt, indem dem Konstruktor ein MainViewModel übergeben wird. Dem Konstruktor des MainViewModels wiederum wird eine FriendDataProvider-Instanz übergeben (Listing 9). Schließlich wird am Ende die Show-Methode aufgerufen, um das Fenster anzuzeigen.

public partial class App : Application
{
  protected override void OnStartup(StartupEventArgs e)
  {
    base.OnStartup(e);
    var mainWindow = new MainWindow(
      new MainViewModel(
        new FriendDataProvider()));
    mainWindow.Show();
  }
}

Der Code aus Listing 9 hat jetzt ein kleines Problem: Jedes Mal, wenn sich der Konstruktor des MainWindow, des MainViewModels oder des FriendDataProviders ändert, muss diese Stelle entsprechend angepasst werden. Das lässt sich mit einem Dependency-Injection-Framework verhindern. Denn genau das passiert in Listing 9: In das MainWindow wird eine MainViewModel-Abhängigkeit injiziert, in das MainViewModel wird ein IFriendDataProvider injiziert.

Ein Dependency-Injection-Framework kann diese Injektionen selbst vornehmen, indem ihm mittgeteilt wird, welche konkreten Typen für welche abstrakten Typen injiziert werden sollen.

Auf dem Markt gibt es zahlreiche dieser Frameworks, ein sehr beliebtes ist Autofac. Um es einzusetzen, wird im Projekt eine Bootstrapper-Klasse erstellt, die eine Bootstrap-Methode enthält, die wiederum einen IContainer zurückgibt. Der IContainer hat die Informationen über die zu erstellenden Typen. Zum Erstellen des IContainer kommt bei Autofac die in Listing 10 verwendete ContainerBuilder-Klasse zum Einsatz. Auf einer ContainerBuilder-Instanz werden mit RegisterType verschiedene Typen registriert, am Ende wird mit der Build-Methode ein IContainer erstellt. Listing 10 zeigt, wie das MainWindow und das MainViewModel mit der Methode AsSelf registriert werden. Das bedeutet, wo auch immer diese konkreten Typen benötigt werden, kann Autofac sie erstellen und/oder injizieren. Der Typ FriendDataProvider wird ebenfalls registriert. Mit der As-Methode wird festgelegt, dass eine FriendDataProvider-Instanz immer dann verwendet wird, wenn eine IFriendDataProvider-Instanz benötigt wird.

public class Bootstrapper
{
  public IContainer Bootstrap()
  {	
    var builder = new ContainerBuilder();

    builder.RegisterType().AsSelf();
    builder.RegisterType().AsSelf();
    builder.RegisterType().As();

    return builder.Build();
  }
}

Der erstellte Bootstrapper ist fertig und kann nun in der OnStartup-Methode der App-Klasse genutzt werden, um den IContainer zu erstellen und schließlich das MainWindow zu erzeugen (Listing 11). Was dabei jetzt auffällt, ist, dass kein Konstruktor mehr aufgerufen wird, um das MainWindow zu erzeugen. Stattdessen wird die Resolve-Methode verwendet – eine Extension-Methode für den IContainer, die im Namespace Autofac untergebracht ist. Mit der Resolve-Methode wird eine neue MainWindow-Instanz erzeugt; wenn sich jetzt der Konstruktor ändert, müssen die Abhängigkeiten lediglich im Container registriert werden – was in der Bootstrapper-Klasse passiert. Eine Anpassung der OnStartup-Methode ist nicht mehr notwendig, da das Dependency-Injection-Framework den Rest übernimmt.

public partial class App : Application
{
  protected override void OnStartup(StartupEventArgs e)
  {
    base.OnStartup(e);

    var bootstrapper = new Bootstrapper();
    var container = bootstrapper.Bootstrap();

    var mainWindow = container.Resolve();
    mainWindow.Show();
  }
}

Methodenaufrufe testen

Wird das ViewModel erweitert, kommt man schnell an den Punkt, an dem Methodenaufrufe getestet werden müssen. Beispielsweise zeigt Listing 12 ein DeleteCommand im MainViewModel. In der OnDeleteExecute-Methode wird der selektierte Freund verwendet und dessen ID an die DeleteFriend-Methode des DataProviders übergeben. Anschließend wird das Friend-Objekt aus der Friends Collection entfernt und die SelectedFriend Property auf null gesetzt. Doch wie lässt sich all das testen?

public class MainViewModel:ViewModelBase
{
  public MainViewModel(IFriendDataProvider dataProvider)
  {
    ...
    DeleteCommand = new DelegateCommand(OnDeleteExecute, OnDeleteCanExecute);
  }


  public ICommand DeleteCommand { get; }

  private void OnDeleteExecute(object obj)
  {
    var friendToDelete = SelectedFriend;

    _dataProvider.DeleteFriend(friendToDelete.Id);
    Friends.Remove(friendToDelete);
    SelectedFriend = null;
  }
  ...
}

Listing 13 zeigt eine Testmethode, die genau den Inhalt der OnDeleteExecute-Methode aus Listing 12 testet. Interessant sind dabei die letzten drei Anweisungen, nachdem das DeleteCommand mit der Execute-Methode ausgeführt wurde. Zuerst wird auf der in der Variablen dataProviderMock gespeicherten Mock-Instanz mithilfe der Verify-Methode geprüft, ob die DeleteFriend-Methode genau einmal aufgerufen wurde – und zwar genau mit der ID des zu löschenden Freundes (friendToDelete). Anschließend wird geprüft, ob der zu löschende Freund aus der Friends Collection entfernt und die SelectedFriend Property wieder auf null gesetzt wurde.

[Fact]
public void ShouldDeleteAndRemoveFriendWhenDeleteCommandIsExecuted()
{
  var dataProviderMock = new Mock();

  dataProviderMock.Setup(dp => dp.LoadFriends())
    .Returns(new List
    {
      new Friend {Id = 1, FirstName = "Thomas"},
      new Friend {Id = 1, FirstName = "Julia"}
    });
  IFriendDataProvider dataProvider = dataProviderMock.Object;
  var viewModel = new MainViewModel(dataProvider);

  viewModel.Load();

  var friendToDelete = viewModel.Friends.First();
  viewModel.SelectedFriend = friendToDelete;

  viewModel.DeleteCommand.Execute(null);

  dataProviderMock.Verify(dp => dp.DeleteFriend(friendToDelete.Id), Times.Once);
  Assert.False(viewModel.Friends.Contains(friendToDelete));
  Assert.Null(viewModel.SelectedFriend);
}

Dialoge anzeigen

Was beim Laden der Daten galt, um das ViewModel testbar zu gestalten, gilt auch für das Anzeigen von Dialogen: Abhängigkeiten müssen abstrahiert und aus dem ViewModel entfernt werden. Ein typischer Fall ist das Anzeigen einer Ok Cancel MessageBox beim Löschen eines Freundes. In Listing 12 wurde die OnDeleteExecute-Methode gezeigt. In diese Methode lässt sich eine MessageBox wie in Listing 14 integrieren; die Anwendung wird wunderbar funktionieren. Das Problem ist nur, dass die MessageBox auch in jedem Unit-Test angezeigt wird, in dem das DeleteCommand ausgeführt wird; beispielsweise in dem Unit-Test aus Listing 13. Der Unit-Test ist somit blockiert – Visual Studio zeigt beim Ausführen des Unit-Tests die MessageBox an. Ein Build-Server, der die Unit-Tests ausführt, wird einfach blockiert sein. Somit gilt auch an dieser Stelle: Die MessageBox muss aus dem ViewModel abstrahiert werden.

private void OnDeleteExecute(object obj)
{
  var result = MessageBox.Show( 
    "Möchten Sie den Freund wirklich löschen?", 
    "Löschen",
    MessageBoxButton.OKCancel);

  if (result == MessageBoxResult.OK)
  {
    var friendToDelete = SelectedFriend;

    _dataProvider.DeleteFriend(friendToDelete.Id);
    Friends.Remove(friendToDelete);
    SelectedFriend = null;
  }
}

Um die MessageBox zu abstrahieren, wird das Interface IMessageBoxService eingeführt:

public interface IMessageBoxService
{
  MessageBoxResult Show(string message, string title, MessageBoxButton buttons);
}

Das MainViewModel wird erweitert, ein neuer Konstruktorparameter vom Typ IMessageBoxService kommt hinzu. Die erhaltene Instanz wird in einer Instanzvariablen gespeichert und in der OnDeleteExecute-Methode verwendet, was in Listing 15 zu sehen ist.

public class MainViewModel:ViewModelBase
{
  private readonly IMessageBoxService _messageBoxService;
  ...

  public MainViewModel(IFriendDataProvider dataProvider,
    IMessageBoxService messageBoxService)
  {
    _messageBoxService = messageBoxService;
    ...
  }

  private void OnDeleteExecute(object obj)
  {
    var result = _messageBoxService.Show( 
      "Möchten Sie den Freund wirklich löschen?", 
      "Löschen",
      MessageBoxButton.OKCancel);

    if (result == MessageBoxResult.OK)
    {
       ...
    }
  }

Mit der Abstraktion der MessageBox in einen IMessageBoxService lässt sich dieser IMessageBoxService im Unit-Test jetzt wunderbar „mocken“. Beispielsweise zeigt das folgende Codebeispiel, wie das MainViewModel mit einem IMessageBoxService erstellt wird. Dabei wird die Methode Show des IMessageBoxService aufgesetzt:

var messageBoxServiceMock = new Mock();
messageBoxServiceMock.Setup(m => m.Show(It.IsAny(),
  It.IsAny(), 
  MessageBoxButton.OKCancel))
  .Returns(MessageBoxResult.OK);
var viewModel = new MainViewModel(dataProvider, messageBoxServiceMock.Object);

Zu beachten ist der Einsatz der Klasse It, die auch zur Moq Library gehört. Mit der generischen It.IsAny-Methode wird festgelegt, dass dieses Set-up der IMessageBoxService.Show-Methode für einen beliebigen String als ersten Parameter und einen beliebigen String als zweiten Parameter gilt. Als dritter Parameter muss MessageBoxButton.OKCancel angegeben werden, und dann wird das Ergebnis MessageBoxResult.OK zurückgegeben.

Mit diesem Code wird im Unit-Test jetzt keine MessageBox mehr angezeigt. Für den produktiven Code ist noch eine Implementierung von ImessageBoxService notwendig. Das kann einfach wie folgt aussehen:

public class MessageBoxService : IMessageBoxService
{
  public MessageBoxResult Show(string message, string title, MessageBoxButton buttons)
  {
    return MessageBox.Show(message, title, buttons);
  }
}

Damit die Anwendung läuft, ist der MessageBoxService lediglich noch im Container von Autofac zu registrieren, damit dieser als IMessageBoxService in das MainViewModel injiziert wird. Dazu wird in der Bootstrap-Methode des Bootstrappers RegisterType auf dem ContainerBuilder aufgerufen (Listing 16).

public IContainer Bootstrap()
{
  var builder = new ContainerBuilder();

  builder.RegisterType().As();

  builder.RegisterType().AsSelf();
  builder.RegisterType().AsSelf();
  builder.RegisterType().As();

  return builder.Build();
}

Einfach F5 drücken, und die Anwendung startet und läuft ohne weitere Anpassungen mit dem neuen MessageBoxService.

Fazit

Dieser Artikel hat gezeigt, wie sich testbare ViewModels erstellen lassen. Dabei ist das Prinzip ganz einfach: Abhängigkeiten werden mithilfe von Interfaces abstrahiert und als Konstruktorparameter definiert. Dadurch lassen sich ViewModels mit 100 Prozent Test-Coverage entwickeln. Allerdings sollte man dazu sagen, dass sich Entwickler auf die komplexen Teile konzentrieren sollten. Das Laden von Freunden und das Anzeigen einer MessageBox wurde in diesem Artikel beispielhaft aufgeführt. In der Praxis empfehlen sich immer diese Stellen für Unit-Tests, an denen man als Entwickler darüber nachdenkt, doch den einen oder anderen Kommentar zu schreiben. Das Offensichtliche zu testen, ist nicht immer lohnenswert, aber das Herzstück zu testen, ist sehr zu empfehlen.

Zur weiteren Beschäftigung mit dem Thema verweise ich auf meine Vorlesung „TDD und MVVM“ auf pluralsight.com.

Windows Developer

Windows DeveloperDieser Artikel ist im Windows Developer erschienen. Windows Developer informiert umfassend und herstellerneutral über neue Trends und Möglichkeiten der Software- und Systementwicklung rund um Microsoft-Technologien.

Natürlich können Sie den Windows Developer über den entwickler.kiosk auch digital im Browser oder auf Ihren Android- und iOS-Devices lesen. In unserem Shop ist der Windows Developer ferner im Abonnement oder als Einzelheft erhältlich.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -