Dieser Artikel dreht sich um’s Testen. Microsoft liefert mit MS Test bereits ein Framework aus, um Anwendungen zu testen. Wir werden und MS Test und NUnit anschauen und verschiedene Tests vergleichen.

AAA-Pattern

Zu Beginn möchte ich kurz das AAA-Pattern erklären. AAA steht für Arrange, Act, Assert. Das Bedeutet, dass zuerst Variablen und Mocks (sofern notwendig) definiert werden. Anschließend erfolgt der zu testende Funktionsaufruf. Abschließend wird das Ergebnis mit Asserts geprüft.

Minimalbeispiel eines Tests nach AAA:

public void Test1() {
  // Arrange
  int x = 5;
  int y = 6;
  int expected = 11;
  MyCalculator target = new MyCalculator();
  
  // Act
  int result = target.Add(x, y);
  
  // Assert
  Assert.AreEqual(expected, result);
}

Der Vorteil des Pattern ist ersichtlich. Es gibt eine klare Trennung zwischen Eingabedaten, Verarbeitung/Ausführung und dem Prüfen des Ergebnisses. Zudem sorgt der einheitliche Aufbau dafür, dass auch andere Entwickler den Test schnell verstehen und ändern können.

Beispiel

Nehmen wir als Beispiel folgendes Beispiel an: Wir haben eine Math-Klasse, welches Zahlen berechnen soll. Zu Beginn sind nur Add und Divide vorhanden. Ein Rumpf für Multiply ist bereits angelegt:

public class MyMath {
  public int Add(int x, int y) {
    return x + y;
  }

  public double Divide(int x, int y) {
    return (double) x / (double) y;
  }

  public int Multiply(int x, int y) {
    throw new NotImplementedException();
    // return x * y;
  }
}

Anhand dieser Klasse bauen wir uns nun unsere Tests auf.

MS-Test in Aktion

using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class MathTest_MS {

  [TestMethod]
  public void AddTest() {
    int x = 5;
    int y = 6;
    int expected = 11;

    MyMath target = new MyMath();
    int result = target.Add(x, y);

    Assert.AreEqual(expected, result);
  }

  [TestMethod]
  public void DivideTest() {
    int x = 10;
    int y = 2;
    double expected = 5;

    MyMath target = new MyMath();
    double result = target.Divide(x, y);

    Assert.AreEqual(expected, result);
  }

  [TestMethod]
  public void DivideTest_DivideByZero() {
    int x = 10;
    int y = 0;
    double expected = double.PositiveInfinity;

    MyMath target = new MyMath();
    double result = target.Divide(x, y);

    Assert.AreEqual(expected, result);
  }

  [TestMethod]
  public void MultiplyTest() {
    int x1 = 5;
    int y1 = 10;
    int expected1 = 50;

    int x2 = -4;
    int y2 = 12;
    int expected2 = -48;

    MyMath target = new MyMath();
    int result1 = target.Multiply(x1, y1);
    int result2 = target.Multiply(x2, y2);

    Assert.AreEqual(expected1, result1, "Error result 1");
    Assert.AreEqual(expected2, result2, "Error result 2");
  }
}

Wir haben verschiedene Tests erstellt. Ob die Prüfung bei „geteilt durch 0“ so korrekt ist, oder doch lieber eine Ausnahme ausgelöst werden sollte, muss jeder selbst entscheiden. In diesem Beispiel habe ich mich dafür entschieden, eine Prüfung auf „geteilt durch null“ auszulassen. Wenn ich etwas durch 0 teile, wird der Wert „unendlich“ ausgegeben. Mir ist bewusst, dass ein „geteilt durch null“ mathematisch nicht korrekt ist 🙂

Die Testklasse muss public sein und die Annotation TestClass beinhalten. Einzelne Tests werden mit TestMethod dekoriert. Nach dem AAA-Pattern werden erst die Zuordnungen gemacht. Anschließend erfolgt die Berechnung und die Prüfung mit Assert. In MultiplyTest teste ich die Methode auf 2 verschiedene Multiplikationen.
Eine Anleitung, um es bestmöglich zu machen, habe ich nicht gefunden. Es bietet sich aber an jeweils für eine Multiplikation von positiven, negativen und gemischten Werten eigene Funktionen zu machen.

Die Zeichenkette am Ende vom Assert wird ausgegeben, falls eine Multiplikation fehlschlägt. Hierdurch können wir besser Unterscheiden, welches Ergebnis fehlerhaft ist.

NUnit in Aktion

Zuerst müssen wir NUnit über NuGet installieren. Ggfs. Ist auch der NUnit3TestAdapter notwendig. Dieser kann ebenfalls über den NuGet-Manager im Visual Studio installiert werden. Die aktuelle NUnit Version ist 3.7.1.

Der Code sieht ähnlich zu dem Beispiel von MS Test aus:

using NUnit.Framework;

[TestFixture]
public class MathTest_NUnit {

  [Test]
  public void AddTest() {
    int x = 5;
    int y = 6;
    int expected = 11;

    MyMath target = new MyMath();
    int result = target.Add(x, y);

    Assert.AreEqual(expected, result);
  }

  [Test]
  public void DivideTest() {
    int x = 10;
    int y = 2;
    double expected = 5;

    MyMath target = new MyMath();
    double result = target.Divide(x, y);

    Assert.AreEqual(expected, result);
  }

  [Test]
  public void DivideTest_DivideByZero() {
    int x = 10;
    int y = 0;
    double expected = double.PositiveInfinity;

    MyMath target = new MyMath();
    double result = target.Divide(x, y);

    Assert.AreEqual(expected, result);
  }

  [Test]
  public void MultiplyTest() {
    int x1 = 5;
    int y1 = 10;
    int expected1 = 50;

    int x2 = -4;
    int y2 = 12;
    int expected2 = -48;

    MyMath target = new MyMath();
    int result1 = target.Multiply(x1, y1);
    int result2 = target.Multiply(x2, y2);

    Assert.AreEqual(expected1, result1);
    Assert.AreEqual(expected2, result2);
  }
}

Ein Vorteil bietet NUnit an dieser Stelle. Mit TestCase können wir verschiedene gleiche Tests zusammenfassen. Dies machen wir nun für MultiplyTest. Das Ergebnis sieht folgendermaßen aus:

[TestCase(5, 10, 50)]
[TestCase(-4, 12, -48)]
public void MultiplyTestFacts(int x, int y, int expected) {
  MyMath target = new MyMath();
  int result = target.Multiply(x, y);

  Assert.AreEqual(expected, result);
}

Das TestCase Attribut bietet die Möglichkeit, einer Testmethode Parameter zu übergeben. Hier sind es x, y und expected. Innerhalb der Methode kann ich auf diese Parameter zugreifen und die Auswertung durchführen. Durch diese Anwendung lässt sich hier ein Test halbieren. Wenn wir 3 neue Tests hinzufügen möchten, müssen wir nur 3 TestCase-Zeilen schreiben.

Fazit und Ausblick

Bei dieser relativ einfachen Umgebung gibt es keine signifikanten Unterschiede zwischen den beiden Frameworks. Die TestCases bieten eine schöne Möglichkeit, um gleiche Tests mit verschiedenen Parametern zu verschlanken. Inwieweit es angewendet werden kann, hängt u.U. mit der Menge an Parametern zusammen.

Der Teufel liegt allerdings im Detail. Wir haben uns zum einen nicht das Abfragen einer Exception angezeigt. Details dazu sehen wir uns im nächsten Teil an.

Wie schreibt ihr eure Tests? Habt ihr Anmerkungen oder Hinweise? Ich freue mich über eure Kommentare.