Vom Dogma zum Karma

Testing mit Visual Studio 2012
Kommentare

Unit Testing ist in der modernen und agilen Softwareentwicklung mittlerweile etabliert. In größeren Projekten sind Testing und Reagieren auf Veränderungen fundamental wichtig. Entwickler benötigen schnell Feedback von dem, was sie gerade machen. Unit Tests helfen dabei. Dennoch erfährt das Thema nicht den nötigen Stellenwert. Oftmals wird viel zu spät im Projekt angefangen, die Qualität zu „sichern“. Meistens stellt sich das recht aufwändig dar, und „echte“ Entwickler „testen“ sowieso nicht. Warum man etwas früher über die Implementierung nachdenken sollte und vielleicht sogar zuerst an den Test, zeigt dieser Artikel.

Unit Testing ist – bei Erfindung um ca. 1989 [1] – mittlerweile etabliert und gehört zum guten Stil eines agilen Entwicklers. Wenn Sie die Tests zuerst schreiben, dann setzen Sie sich gedanklich schon mit der Implementierung auseinander und treffen dort quasi Designentscheidungen. Die gute alte Regel „Think First“ ist bei TDD schon eingebaut. Wenn Sie danach die Frage nach dem „Wie teste ich das?“ erfolgreich beantworten, ist der Rest eigentlich nur noch Formsache. Eigentlich! Ein paar Regeln und Konventionen (siehe Kasten: „Ein guter Unit Test“) sollten dann vielleicht doch noch eingehalten werden, damit Sie später von Ihren Tests noch etwas haben. Wenn Sie also

  • die Steigerung der Codequalität
  • höhere Flexibilität und bessere Änderbarkeit des Codes
  • mehr Vertrauen in die Codebasis und schnelles Reagieren auf Veränderungen
  • besseres Design, das auch Kollegen verstehen
  • pünktlich Feierabend
  • integrierte Dokumentation

befürworten, dann ist Unit Testing genau das Richtige für Sie.

Ein guter Unit Test

Um gute Unit Test zu schreiben, brauchen Sie viel Erfahrung und Know-how. Wir zeigen Ihnen, was gute Unit Tests ausmachen und was viele falsch machen.

Was gute Unit Tests ausmacht

  1. Von überall auszuführen: Das heißt auf jedem Entwicklungssystem
  2. Geschwindigkeit: Generell gilt: So schnell wie möglich, ab ca. 200 ms wird es grenzwertig
  3. Keine Interaktion mit Filesystem, Netzwerk, Datenbanken oder externen Systemen: Alles andere sind schon Integrationstests
  4. „Kurz“ und verständlich: Siehe „It’all about Readability”
  5. Orthogonalität von Tests: Tests sind unabhängig voneinander und beeinflussen sich nicht; sie rufen sich nicht gegenseitig auf, stellen alle für den Test benötigten Daten „selbstständig“ zusammen und testen nur das, was in anderen Tests nicht schon getestet wurde; die Unabhängigkeit wird negativ beeinflusst durch andere Systeme, wie Datenbanken, Netzwerke oder Dateisysteme

Was viele falsch machen

  • Testcode schreiben, der sich ständig ändert
  • Zu fragil aufgrund von zu feiner Granularität
  • Testcode schreiben, der nie oder selten ausgeführt wird
  • Das passiert, wenn Code zu lange braucht, um ausgeführt zu werden, wenn Abhängigkeiten zu Drittsystemen bestehen, wie z. B. Datenbanken, auf die man keine Berechtigungen besitzt
  • Keine Integration der Tests in das Build-System
  • Tests, die Abhängigkeiten zu anderen Tests haben
  • Testcode wird als 2nd Citizen Code angesehen
  • Kein Styleguide für Testcode vorhanden; Normen, die für Produktivcode gelten, werden nicht für Tests eingehalten
  • •Es wird zu viel in einer Testmethode getestet
  • Man erkennt das daran, dass es schwer fällt, einen Namen für die Tests zu finden
  • Mehrere Asserts am Ende oder innerhalb des Tests

Beispiel: Die Wechselstube …

Damit Sie beim Lesen einen praxisnahen Bezug erhalten, haben wir ein Szenario aus der Finanz- und Währungswelt kreiert. Es lautet ungefähr so: „Als Kunde in einer Wechselstube soll ich in der Lage sein, Euro und US-Dollar umzutauschen, damit ich in der jeweiligen Währung einkaufen kann.“

Als zentralen Ort haben wir die Wechselstube (ICurrencyExchangeOffice). Das Geld (Money) können Sie Ihrer Geldbörse (Wallet) in verschiedenen Währungen (Currency) hinzufügen (Wallet.Add(Money m)) und wieder herausnehmen (Wallet.Take(Money m). Manchmal möchten Sie auch das gesamte Geld einer bestimmten Währung aus dem Portemonnaie entnehmen (Wallet.TakeAll(ICurrency c)). Mit dem Geld in Ihrer Geldbörse können Sie in die Wechselstube gehen und das Geld in eine andere Währung umtauschen (ICurrencyExchangeOffice.Exhange(Money m, ICurrency to)).

Das Beispiel wurde komplett testgetrieben entwickelt und ist so flexibel, dass Sie leicht andere Währungsumwandlungen hinzufügen können. In Listing 1 sehen Sie die wesentlichen Klassen der Implementierung (siehe auch WindowsDeveloper.Component.dll). Die finalen Tests für diese Implementierung finden Sie als Download in der Komponente WindowsDeveloper.Component.Tests.dll.

Listing 1

public interface ICurrencyExchangeOffice {   IList Currencies { get; }   event EventHandler OnExchangeRateChanged;   Money Exchange(Money money, ICurrency to); }  // vereinfachte Darstellung der Klasse Money public struct Money {   public ICurrency Currency { get; set;};   public double Amount { get; set; };   public Money(double amount, ICurrency currency); }  // vereinfachte Darstellung der Klasse Wallet public class Wallet {   public void Add(Money money);   public Money GetAmountFor(ICurrency currency);   public void Take(Money money);   public Money TakeAll(ICurrency currency); }

It’s all about Readability

Jeder, der bereits Code geschrieben hat, hat sicherlich die Erfahrung gemacht, in alten Code abtauchen zu müssen – mit der Erkenntnis, dass der einst geschriebene Code nicht den besten Programmierkünsten entspricht. Das liegt nicht an der Absicht, schlechten Code zu schreiben, sondern oftmals daran, dass man zum damaligen Zeitpunkt gewisse Dinge einfach nicht besser wusste. Diese Erkenntnis ist viel wert, denn das macht uns bewusst, dass wir hoffentlich dazugelernt haben und etwas mit dem Code nicht stimmt.

Doch was ist es, was den Code schlecht macht? Das kann sich vielfältig ausprägen. An dieser Stelle sei die Lesbarkeit von Code erwähnt, die manchmal von Entwicklern als eher zweitrangig bewertet wird. Denn schließlich sind es „die Features“, für die der Kunde initial zahlt, und wenn die Time-to-Market noch augenscheinlich verringert werden kann, scheint jedes Mittel recht.

Doch wehe der Kunde fordert nach zwei bis drei Monaten Bugfixes und Changes ein. Dann werden die technischen Schulden von früher schmerzliche (Über-)Stunden beim Entwickler verursachen. Hätte man damals doch die Methoden und den Code ein wenig lesbarer gestaltet …

Für Produktivcode scheint es nunmehr klar zu sein, dass Lesbarkeit wichtig ist. Die Zeiten von Aussagen, wie „Weil der Code schwer zu schreiben war, muss er auch schwer zu lesen sein“ sind glücklicherweise für das Gros der Entwickler vorbei. Doch wie steht es um Tests? Grundsätzlich sollte auch Testcode eine gute Lesbarkeit aufweisen. Er ist dem Produktivcode gleichwertig und kein Bürger zweiter Klasse.

Ein gut benannter Test vereinfacht dem Programmierer die Codeänderung und macht die Erfassung der Spezifikation an den Code einfacher. Der Entwickler bekommt ein schnelles, verständliches Bild von dem zu testenden Code, was eine Änderung eventuell für Folgen hat und was noch beachtet werden muss. Auch kann ein fehlschlagender Test darauf hinweisen, dass eine Spezifikation an die bestehende Software geändert werden muss, hilft dabei, Rückfragen vor Auslieferung zu stellen und vermeidet somit Fehler. Die Argumente für eine gute Benennung und somit für bessere Lesbarkeit sind vielfältig. Ein etabliertes Namensmuster für Tests ist Methode/Funktionalität_Szenario_Resultat/ErwartetesVerhalten.

Ein anfangs wenig sprechender Name, wie Feature1, kann somit umbenannt werden, wie in Listing 2 zu sehen.

Listing 2

// Schlechter Name für einen Test [Test] public void Feature1() {...} // Besserer Testname nach Method_Secanrio_Resultat [Test] public void Add_FirstSummandIsTwoSecondSummandIsTree_YieldsFive() { ... }

Der Name ist lang und anfänglich gewöhnungsbedürftig. Wer aber dieser Art von Namen eine Chance gibt, wird merken, dass sie ungemein nützlich sind. Wir halten für uns und unsere Kollegen den Fokus in dem aktuellen Test und die Spezifikation an den Produktivcode fest. Außerdem erleichtern wir es im Falle eines fehlschlagenden Tests demjenigen, der den Testcode „fixen“ muss, sich einen Überblick zu verschaffen. Darüber hinaus hilft die Vereinheitlichung der Namen, bei der Programmierung doppelte Tests bei großen Testklassen zu vermeiden, da Dopplung von Testszenarien schneller auffällt.

Abschließend sei kurz angemerkt, dass das Muster zur Benennung von Testmethoden zu anfangs zwei Teile aufweist, Methode und Funktionalität. Das bedeutet, dass man entweder als Namen den getesteten Methodennamen aus der Klasse verwendet oder vielmehr die Funktionalität benennt, die getestet werden soll. Wir programmieren schließlich Funktionalitäten für eine Software, wobei wir uns Methoden bedienen. Was damit gemeint ist, wird beispielsweise spätestens dann deutlich, wenn in einem Test mehrere Methoden verwendet werden, um Code testen zu können. Hier wird es schwer fallen, sich nur der Namen der getesteten Methoden zu bedienen, natürlicherweise wird man die Funktionalität umschreiben.

Aufmacherbild: Investigator checking test tubes von Shutterstock / Urheberrecht: YanLev

[ header = Testing mit Visual Studio 2012 – Teil 2 ]

KISS: Keep it simple and stupid

Das KISS-Prinzip ist vielen sicherlich geläufig. Im Prinzip beschreibt es, eine möglichst einfache Lösung für ein Problem zu finden. Im weiteren Sinne kann man das Prinzip auch auf Unit Tests übertragen, indem man sie kurz, leicht verständlich und minimalistisch hält. Doch was heißt das?

Minimalistisch: Tests sollen nur das testen, was ihr Name beschreibt, und nur das an Code enthalten, was unbedingt notwendig ist, um den Test erfolgreich durchzuführen.

Leicht verständlich: Das, was auch für den Methodennamen gilt, ist für die in dem Test enthaltenen Variablen genauso wichtig. Indem wir sprechende Namen für Variablen vergeben, ermöglichen wir es, den Inhalt eines Tests so schnell wie möglich begreifbar zu machen. So sollte man, anstatt nur eines „magischen“ Werts in einem Assert-Statement, lieber eine Variable verwenden, die den Wert beschreibt. Lassen Sie uns, bezogen auf unser Geldbörsenbeispiel, überprüfen, ob ein Minimalbetrag in unserem Portemonnaie enthalten ist.

Listing 3

[Test] public void BadlyNamedTest() {   var euro = new Euro();   var wallet = new Wallet();    var money = wallet.GetAmountFor(euro);    Assert.That(money.Amount, Is.EqualTo(10)); }

In Listing 3 überprüfen wir im Assert, ob unsere Geldbörse mit 10 Euro gefüllt ist. Direkt aus dem Code ist es nicht erkennbar, worum es sich bei dem Wert 10 handelt. Vergleichen Sie hierzu Listing 4. Sie werden sehen, dass es besser verständlich ist.

Listing 4

[Test] public void  GetAmountFor_FillWalletWith10Euro_ReturnMinAwaitedMoneyAmountOf10() {   var euro = new EuroCurrency();   var tenEuro = new Money(10, euro);   var wallet = new Wallet();   wallet.Add(tenEuro);    var money = wallet.GetAmountFor(euro);    var minAwaitedMoneyAmount = 10.0;    Assert.That(money.Amount, Is.EqualTo(minAwaitedMoneyAmount)); }

Kurz: Die optimale Länge eines Unit Tests lässt sich nur schwierig als konkreter Wert ausdrücken. Allerdings können wir Indikatoren aufzeigen, die einer dem getesteten Code guten Länge entspricht. Als Daumenregel können Sie sich merken, dass, wenn Sie bei einem Unit Test in einem Editor Ihrer Wahl anfangen zu scrollen, dieser definitiv zu lang ist. Code sollte im wahrsten Sinne des Wortes auf einen Blick überschaubar sein. Ein weiterer Indikator für einen wahrscheinlich zu „langen“ Test ist der Testmethodenname: Fällt es Ihnen schwer, einen Namen zu finden, dann ist das ein Anzeichen dafür, dass der Test zu lang ist, weil Sie eventuell zu viel auf einmal testen. Hier sollte der Test aufgeteilt werden. Sollte auch das nicht möglich sein, weil Sie sehr viel Set-up-Code benötigen und dieser nicht trennbar ist, sollten Sie überprüfen, ob das Design der Klasse Schwächen aufweist.

Can you smell it? Wenn der Testcode riecht …

Im vorherigen Abschnitt sind wir, ohne dies ausdrücklich erwähnt zu haben, auf „Code Smells“, wie zu lange Testmethoden oder schlecht benannte Variablen, eingegangen. Wenn Sie Unit Testing eine Zeit lang praktizieren, werden Sie merken, dass Testcode sich doppelt. Behelfen Sie sich hier! Ziehen Sie den Code in Helfermethoden oder Klassen raus. Sind diese Klassen sogar über die momentan hinausgehende Domäne wiederverwendbar, bietet es sich an, eine hauseigene Testbasis zu schaffen. Aber seien Sie vorsichtig. Werden die Helferklassen komplex, sollten diese auch getestet werden, denn sie bergen auch die Gefahr von Fehlern, die sich in ihren Tests fortpflanzen werden.

Bevor Sie anfangen, eigene, allgemeine Erweiterungen zu schreiben, die Ihnen das Testen erleichtern, schauen Sie am besten in die Dokumentation der heutigen Testframeworks. Sie bieten eine Menge Funktionalität. Des Weiteren lohnt es sich, einen Blick auf die verfügbaren Mocking-Frameworks zu werfen. Sie erleichtern das Arbeiten mit Tests ungemein und vermeiden einiges an unnötigen Code.

NUnit vs. MSTest: fluent or not fluent?

Um Ihre Tests effektiv zu gestalten, gibt es beim Visual Studio zahlreiche Plug-ins, die Ihnen das Leben erleichtern. MSTest ist als Standardframework in Visual Studio integriert. NUnit ist sehr beliebt in der Entwicklercommunity. Beide haben ihren Sinn und werden hier kurz verglichen.

In Tabelle 1 sind die Testattribute gegenübergestellt. Hier ergeben sich einige Unterschiede. Die Benennung ist Geschmackssache, jedoch sind die Namen des NUnit-Frameworks sprechender.

Tabelle 1: Vergleich der Testattribute in MSTest und NUnit

MSTest-Attribute NUnit-Attribute Beschreibung
[TestMethod] [Test] Kennzeichnet einen Unit Test.
Kennzeichnet einen Unit Test. [TestFixture]

Kennzeichnet eine Gruppe von Unit Tests.

Alle Tests und Initialisierungen/CleanUps müssen nach dieser Deklaration auftauchen.

[ClassInitialize]

[ClassCleanup]

[TestFixtureSetUp]

[TestFixtureTearDown]

Kennzeichnet eine Methode, die einmalig vor/nach den Tests in der Testklasse ausgeführt wird.

[TestInitialize]

[TestCleanUp]

[SetUp]

[TearDown]

Kennzeichnet eine Methode, die vor/nach jedem Test ausgeführt wird.

[AssemblyInitialize]

[AssemblyCleanUp]

[SetupFixture] für eine Klasse mit [Setup] und [TearDown] verwenden Kennzeichnet eine Methode, die vor/nach dem ersten/letzten Test innerhalb der Assembly ausgeführt wird.

Das MSTest-Attribut [AssemblyInitialize] lässt sich in NUnit nicht direkt auf einer Methode anwenden. Jedoch kann man das [SetupFixture]-Attribut auf eine Klasse anwenden und die [Setup]- und [Teardown]-Attribute in ihr einbinden und den gleichen Effekt erzielen.

Bei der Auflistung der Asserts in Tabelle 2 sehen Sie die Unterschiede zwischen der MSTest- und der NUnit-Variante. Es gibt zwar viele der MSTest Asserts in NUnit, jedoch verwendet man aktuell das Assert.That-Mantra.

Eine durchgängige Assert.That(object, Is.EqualTo(object-)-Syntax ist wesentlich „flüssiger” als die MSTest-Variante und erhöht die Lesbarkeit.

Tabelle 2: Asserts in MSTest und NUnit

MSTest Asserts NUnit Asserts Beschreibung
Assert.AreEqual(object expected, oject actual) Assert.That(object, Is.EqualTo(expected)) Vergleich von aktuellem mit erwartetem Ergebnis, zahlreiche Überladungen für diverse Typen
[ExpectedException] Attribut verwenden oder try/catch Logik Assert.That(delegate, Throws.TypeOf()); Überprüft, ob eine Ausnahme erzeugt wurde oder nicht
Assert.Fail Markiert einen Test als fehlerhaft
Assert.Inconclusive Markiert einen Test als noch nicht vollständig
Assert.IsNull/Assert.IsNotNull

Assert.That(obj, Is.Null)/

Assert.That(obj, Is.Not.Null)

Überprüfung auf Null bzw. nicht Null

Assert.IsTrue/

Assert.IsFalse

Assert.That(b, Is.True)/

Assert.That(b, Is.False)

Assert.That(b, Is.True)/

Assert.That(b, Is.False)

StringAssert.Contains

Assert.That(str1, Is.StringContaining(str2))/

Assert.That(str, Is.Not.StringContaining(str2))

Überprüft, ob zwei Zeichenketten gleich sind
StringAssert.Matches/ StringAssert.DoesNotMatch

Assert.That(str1, Is.Not.StringMatching(str2))/

Assert.That(str1, Is.Not.StringMatching(str2))

Assert.That(str1, Is.Not.StringMatching(str2))/

Assert.That(str1, Is.Not.StringMatching(str2))

StringAssert.StartsWith

Assert.That(str1, Is.StringStarting(str2))/

Assert.That(str1, Is.StringEnding(str2))

Überprüft, ob eine Zeichenkette mit spezifischen Zeichen (nicht) startet oder (nicht) endet

CollectionAssert.

Contains/CollectionAssert.

DoesNotContain

Assert.That(list1, Is.SubsetOf(list2))/

Assert.That(list1, Is.Not.SubsetOf(list2))

Überprüft, ob eine Collection innerhalb einer anderen enthalten ist oder nicht

CollectionAssert.

AllItemsAreNotNull

Assert.That(list, Has.All.Not.Null) Assert.That(list, Has.All.Not.Null)

CollectionAssert.

AllItemsAreUnique

Assert.That(list, Has.All.Unique) Überprüft, ob alle Elemente einer Collection sich unterscheiden

CollectionAssert.

AreEqual

Assert.That(list1, Is.EqualTo(list2)) Überprüft Reihenfolge und ob alle Elemente in der Zielliste enthalten sind

CollectionAssert.

AreEquivalent

Assert.That(list1, Is.EquivalentTo(list2) Überprüft unabhängig von der Reihenfolge, ob alle Elemente in der Zielliste enthalten sind

Sie entspricht auch dem menschlichen Sprachgebrauch: „Überprüfe ob ObjectA gleich dem ObjectB ist“. Verwendet man dieses Fluent-Api eine gewisse Zeit und hat sich an es gewöhnt, ist die Hürde, sich in die MSTest-Syntax einzuarbeiten, sehr hoch.

Ein weiterer Punkt, der gerne bei MSTest moniert wird, ist die Überprüfung von Exceptions. Um eine Exception mit MSTest-Bordmitteln zu überprüfen, benötigt man nicht ein „einfaches“ Assert, wie bei anderen Überprüfungen, sondern ein [ExpectedException]-Attribut oder ein try/catch-Konstrukt. NUnit hat hier die durchgängige Methodik eines Assert.That(delegate d, Throws.TypeOf()). In Listing 5 sind die beiden Varianten kurz gegenübergestellt.

Listing 5

// MSTest Assert für Exception aus WalletTests.cs [TestMethod] [ExpectedException(typeof(ArgumentNullException))] public void GetAmountFor_DefaultWallet_ThrowsArgumentException() {   var wallet = new Wallet();   wallet.GetAmountFor(null); } // NUnit Assert für Exception [Test] public void GetAmountFor_DefaultWallet_ThrowsArgumentException() {   var wallet = new Wallet();   Assert.That(() => wallet.GetAmountFor(null),    Throws.Typeof()); }

Um die Assert.That–Methodik in MSTest zu nutzen bzw. die Tests zu starten, benötigt man neben der Referenz zu NUnit einen eigenen NUnit Test Runner. Wer dies nicht möchte und MSTest als Test Runner und NUnit mit dem Assert.That-Mantra verwenden will, dem sei [7] empfohlen. Mit einem zusätzlichen using-Konstrukt wie using Assert = NUnit.Framework.Assert können Sie flüssige Asserts aus MSTest herausschreiben und besitzen die volle MS-Integration für Build-System, Test-Reporting und Coverage-Analyse.

[ header = Testing mit Visual Studio 2012 – Teil 3 ]

Faking mit dem Nashorn

Wer testet, wird alsbald auf Abhängigkeiten in seinem Code stoßen, die das Testen erschweren werden. Idealerweise schneidet ein Entwickler die zu testende Klasse (CUT: class under test) so, dass man die Anhängigkeiten bspw. über Schnittstellen entkoppeln kann. Das bewirkt, dass unter anderem der Code testbarer wird. Jedoch müssen wir uns Gedanken machen, wie wir das Verhalten der entkoppelten Abhängigkeit simulieren. Im Falle eines Interface ist dessen erstmals intuitiver Weg in einer Behelfsklasse zu implementieren, was wiederum mehr Testcode bedeutet. Das ist nicht notwendig, da genügend Mocking-Frameworks existieren, die diese Aufgabe für uns übernehmen. Wir haben uns im Zuge dieses Artikels für Rhino.Mocks [4] entschieden. So sagen wir Rhino.Mocks, wie in Listing 6 zu sehen, dass wir ein Objekt bzw. Stub benötigen, das das Interface ICurrencyExchangeOffice implementiert. Zudem bestimmen wir, dass das Property Currencies, das vom Typen IList ist, zwei Währungen zurückgibt. Im Hintergrund generiert Rhino.Mocks ein Proxy-Objekt, das sich so verhält, wie wir es definiert haben. Nun können wir uns wieder auf unseren Test konzentrieren und die Wechselstube wie jedes andere beliebige Objekt verwenden.

Listing 6

var exchangeOffice = MockRepository.GenerateStub(); exchangeOffice.Expect(cs => cs.Currencies).Return(new List {   new EuroCurrency(),   new USDCurrency() }).Repeat.Any();

Rhino.Mocks setzt eine bestimmte Terminologie ein, um die unterschiedlichen Fake-Typen zu differenzieren. So müssen Sie zwischen diesen beiden unterscheiden:

  1. Stubs: Ersetzt eine Abhängigkeit in Ihrem Code und ermöglicht Ihnen, es in einer von Ihnen definierten Weise zu kontrollieren. Es gibt lediglich Werte für Methoden und Properties wieder oder wirft Exceptions. Stubs werden meistens im Zusammenhang mit State-based Tests [6] verwendet.
  2. Mocks: Besitzt alle Eigenschaften eines Stubs und kann für dieselben Aufgaben verwendet werden. Jedoch besitzt es eine wichtige und damit wesentliche Funktion mehr: Mocks können die auf ihnen ausgeführten Interaktionen aufzeichnen. Sie sind eine Art Rekorder für Codeaufrufe, die am Ende eines Tests überprüft werden können. Mocks werden im Zusammenhang mit Interaction-based Tests [6] verwendet.

Andere Frameworks setzen identische oder teilweise weitere Begriffe ein. Schauen Sie sich hierzu bspw. den XUnit-Terminologieüberblick unter [5] an. Wir empfehlen hierbei allerdings eine einfache Begrifflichkeit bestehend aus Fake, Stubs und Mocks zu verwenden, denn eine weitere Untergliederung stiftet nur Verwirrung.

Integrationstests vs. Unit Tests

Nun haben Sie eine ganze Menge Unit Tests geschrieben und wissen auch, wie Sie diese erfolgreich und effizient erstellen, aber irgendetwas fehlt. Jede Komponente oder jedes Modul einzeln für sich zu testen, ist im bedingten Maße ausreichend. Oft treten Probleme erst im Zusammenspiel der Einzelteile auf. Eine Datenbank oder ein Filesystem „wegzufaken“ ist auf jeden Fall nicht einfach und benötigt Zeit und Erfahrung. Mit richtigen Daten und anderen Rahmenbedingungen verhält sich ein System manchmal doch etwas anders.

Kurz: Sie benötigen Integrationstests

Für Integrationstests gelten andere Regeln: Sie dürfen das Dateisystem verwenden. Sie dürfen eine Datenbank verwenden. Sie dürfen Web-Service-Aufrufe in Ihrem Test ausführen. Jedoch sollten sie dabei ganz klare Abgrenzungen zu Ihren bestehenden Unit Tests machen und diese gesondert betrachten. Eine bewährte und einfache Methode ist schlicht, einen eigenen Ordner im Visual-Studio-Projekt namens UnitTests und IntegrationsTests anzulegen und dort die jeweiligen Tests der entsprechenden Kategorie abzulegen. Dies erhöht die Nachvollziehbarkeit und fördert die Verständlichkeit. Die MSTest-Attribute TestCategory und TestProperty helfen Ihnen ebenfalls bei der Kennzeichnung der Integrationstests. In Listing 7 sind diese Properties dargestellt.

Wenn Sie damit beginnen, „echte“ Daten und Systeme zu verwenden, tritt schnell das Problem der Abhängigkeiten der Tests von den Daten auf. Wenn Sie z. B. CRUD-Methoden für ein Repository mit Daten aus der Datenbank testen möchten, sollte jeder Test unabhängig sein und nicht auf Ergebnisse anderer Tests aufbauen. Im Beispiel Datenbank ist es ratsam, in der Set-up-Logik die Datenbasis jedes Mal komplett neu aufzubauen und einen konsistenten Datenstand herzustellen. Wenn Sie z. B. mit dem Entity Framework arbeiten, können Sie dies durch Implementierung des Interface IDatabaseInitialize und der Methode Seed(DBContext context) für Ihr Repository erreichen. Mit den Attributen TestInitialize bzw. ClassInitialize können Sie den Zeitpunkt des Aufbaus Ihrer Testdaten gezielt steuern.

Listing 7

// Beispiel für TestAttribute Coded UI Test aus IEBrowserTests.cs [CodedUITest] public class IEBrowserTests {   public IEBrowserTests(){ }    [TestMethod]   [TestCategory("UIIntegration")]   public void NavigateNews_IEBrowserNaviagtingToWindowsDeveloperNewsPage_ReturningNewsPage()   {     this.UIMap.NavigateIEToWindowsDeveloper();     this.UIMap.CloseIEBrowser();   }   // usw. ... }

Wenn Ihre Solution im Laufe der Entwicklung zahlreiche Tests enthält, hilft Ihnen der Test Explorer bei der Filterung und Auswahl der jeweiligen Tests. Die einfache Funktion GroupBy des Kontextmenüs liefert das Sortieren der Tests nach Class, Duration, Outcome, Traits und Project. Besonders die Filter Traits und Duration sind sehr hilfreich (Abb. 1).

Abb. 1: GroupBy der Tests im Test Explorer

Gerade die Zeiten sollte man bei Integrationstests im Blick behalten und sofort agieren, wenn sie rasant ansteigen.

Ein echtes Killerfeature ist die Suche mit KeyWords innerhalb des Test Explorers (Abb. 2). Nahezu jede Konstellation und Filterbedingung lässt sich abbilden. Wenn Sie z. B. alle Tests der Klasse Money erhalten möchten, geben Sie: Class: „Money“ ein. Es können auch Kombinationen und Negationen eingegeben werden. Alle Tests mit Trait IntegrationTests, außer der Klasse Wallet, lassen sich mit Traits: „IntegrationTests“ –class: „Wallet“ ermitteln. Somit können Sie gezielt Ihre Tests ausführen und erhalten schnell einen Überblick, was gerade funktioniert und was nicht. Diese Ergebnisse der Filter lassen sich in sog. Playlists speichern. Wenn Sie eine Auswahl von Tests öfter benötigen, können Sie mit Rechtsklick auf ADD TO PLAYLIST | NEW PLAYLIST eine Datei erzeugen, die der Test Explorer für Sie verwaltet.

Abb. 2: Filter nach „KeyWords“ im Test Explorer

UI Testing mit Visual Studio 2012

Die Oberfläche bzw. das UI eines Systems wird oftmals zum Schluss eines Projekts getestet. Durch viel manuelles „Zusammengeklicke“ wird sichergestellt, dass die zentralen Eingabemasken funktionieren. Das kann sehr zeitintensiv und somit kostspielig werden. Visual Studio bietet mit Coded UI Tests eine gute Möglichkeit, diese UI-Integrationstests zu automatisieren. Sie sollen hier kurz vorgestellt werden und sind als Ergänzung zu Unit Tests zu verstehen. Die verschiedenen Szenarien mit und ohne Coded UI Tests sind in Abbildung 3 dargestellt.

Abb. 3: Verschiedene Einsatzszenarien von Code UI Tests (Quelle: http://msdn.microsoft.com/en-us/library/dd286726.aspx)

Wenn die Architektur des Systems es zulässt, kann man die Businessschicht komplett „faken“ und frühzeitig die UI-Entwurfsphase beginnen. In unserer Demo Solution ist ein Beispiel für einen Coded UI Test angefügt. Er enthält einen einfachen Browseraufruf (www.windowsdeveloper.de) und eine Überprüfung, ob ein User Control an der erwarteten Stelle zu finden ist (Assert). Danach wird der Browser wieder geschlossen. Der Coded UI Test Builder unterstützt Sie bei dieser Tätigkeit sehr gut, und es empfiehlt sich, diese Möglichkeit der UI-Automatisierung im nächsten Projekt in Erwägung zu ziehen.

Fazit

Unit Testing mit Visual Studio 2012 liefert echte Mehrwerte im Projektalltag. Die richtigen Methoden und Techniken helfen Ihnen, effizient dabei zu sein. NUnits Fluent-Api integriert sich mit seinem durchgängigen Assert.That-Mantra optimal in das TDD-Karma von heute. Zusätzliche UI-Integrationstests steigern dabei die Testabdeckung.

Unit Testing folgt keinen harten Regeln, die einen perfekten Test garantieren. Es ist kein Dogma. Alle gezeigten Techniken und Empfehlungen sind flexibel und helfen dabei, bessere Unit Tests und Code zu schreiben.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -