Zum Inhalt
wannacode

Tests mit xUnit

Kurz gesagt: Ein Unit-Test ist Code, der bei jeder Änderung ein kleines Stück prüft, meist eine Methode; in C# schreibst du ihn mit xUnit, [Fact] und Assert.Equal.

Problem

Der Rabatt, der leise kaputtging

Die Regel des Shops lautet: Der Rabatt ist in Prozent, und bei einem Einkauf ab 100 € kommen noch 5% dazu. Du schreibst die Methode und probierst sie von Hand aus:

Program.cs
class Discount
{
// Rabatt in Prozent. Ab 100 € kommen noch 5% dazu.
public static double Apply(double price, int percent)
{
if (price > 100)
{
percent = percent + 5;
}
return price * (100 - percent) / 100;
}
}
Console.WriteLine(Discount.Apply(50, 10));
Console.WriteLine(Discount.Apply(200, 10));
Ausgabe
45
170

50 € mit 10% Rabatt sind 45 €, und 200 € mit 15% sind 170 €. Das sieht richtig aus, also gibst du es frei. Eine Woche später zahlt eine Kundin mit einem Einkauf von genau 100 € 90 € statt 85 €. Die Bedingung ist price > 100, sie müsste price >= 100 sein.

Niemand hat genau 100 € ausprobiert. Und bei jeder weiteren Änderung an der Methode musst du alle Fälle wieder von Hand ausprobieren.

Die Idee

Code, der Code prüft

Statt die Ausgabe mit eigenen Augen zu lesen, schreibst du kleine Methoden, die deinen Code aufrufen und das Ergebnis mit dem erwarteten vergleichen. Das ist ein Test. Du führst alle Tests mit einem Klick aus, nach jeder Änderung, und sie sagen dir, was kaputtgegangen ist.

Art Was sie prüft Beispiel
Unit-Test ein kleines Stück, meist eine Methode, getrennt vom Rest Discount.Apply(100, 10) gibt 85 zurück
Integrationstest wie die Stücke zusammenarbeiten: mit einer Datenbank, einer Datei, einem anderen Dienst die Bestellung wird in der Datenbank gespeichert und wieder gelesen
End-to-End-Test die ganze Anwendung, so wie ein Mensch sie benutzt die Kundin legt etwas in den Warenkorb, bezahlt und bekommt eine E-Mail
In dieser Lektion schreiben wir Unit-Tests. In C# ist die Bibliothek, die dafür am häufigsten benutzt wird, xUnit.

Warum es sich lohnt:

  • Du siehst den Fehler sofort, solange du noch weißt, was du geändert hast, und nicht erst, wenn sich die Kundin beschwert;
  • der Test zeigt, wie der Code benutzt wird: welcher Fall was zurückgibt;
  • du kannst den Code mutig aufräumen. Wenn die Tests nach der Änderung immer noch grün sind, hat sich das Verhalten nicht geändert.
Genauer, für Neugierige

Es gibt auch andere Arten von Tests: funktionale (erfüllt das Programm die Anforderungen), Performancetests (wie viele Anfragen es aushält), Sicherheitstests (gibt es Lücken) und Tests der Benutzeroberfläche (die Buttons, die Formulare). Alle bauen auf Unit-Tests auf: Von ihnen gibt es die meisten, und sie sind am schnellsten und am billigsten zu pflegen.

Neben xUnit wirst du in .NET NUnit ([Test], [SetUp]) und MSTest ([TestMethod], [TestInitialize]) begegnen. Die Ideen sind dieselben. Hier nehmen wir xUnit: Das ist auch das, was dir die Vorlage dotnet new xunit gibt.

Sieh es dir an

Der erste Test

Die Tests für Discount stehen in einer eigenen Klasse, DiscountTest, in einem eigenen Testprojekt:
DiscountTest.cs
using Xunit;
public class DiscountTest
{
[Fact]
public void TenPercentOff()
{
Assert.Equal(45.0, Discount.Apply(50, 10));
}
[Fact]
public void ExtraFivePercentFrom100()
{
Assert.Equal(85.0, Discount.Apply(100, 10));
}
}
  • [Fact] vor der Methode sagt xUnit „das ist ein Test, führe ihn aus“. Solche Etiketten in eckigen Klammern heißen Attribute. Wie xUnit sie findet, siehst du in der letzten Lektion des Moduls.
  • Die Testklasse ist public: xUnit führt nur Tests aus öffentlichen Klassen aus. using Xunit; ganz oben gibt dir Fact und Assert.
  • Assert.Equal(erwartet, tatsächlich) vergleicht die beiden Werte. Wenn sie verschieden sind, schlägt der Test fehl.

Wir führen die beiden Tests gegen den Code aus der ersten Karte aus:

Ergebnis
FEHLER ExtraFivePercentFrom100
Assert.Equal() Failure: Values differ
Expected: 85
Actual: 90
OK TenPercentOff
Tests: 2, bestanden: 1, fehlgeschlagen: 1

Der erste Test besteht. Der zweite schlägt fehl und sagt genau, was passiert ist: Er hat 85 erwartet, und die Methode hat 90 zurückgegeben. In der Ausgabe von xUnit ist der erwartete Wert Expected und der zurückgegebene Actual. Du änderst die Bedingung in price >= 100, führst sie noch einmal aus, und beide sind grün:

Ergebnis
OK ExtraFivePercentFrom100
OK TenPercentOff
Tests: 2, bestanden: 2, fehlgeschlagen: 0

Jeder Test macht drei Dinge: Er bereitet die Werte vor, ruft den Code auf und prüft das Ergebnis.

public class DiscountTest
{
[Fact]
public void TenPercentOff()
{
Assert.Equal(45.0, Discount.Apply(50, 10));
}
[Fact]
public void ExtraFivePercentFrom100()
{
Assert.Equal(85.0, Discount.Apply(100, 10));
}
}
Schritt für Schritt, in Worten
  1. DiscountTest hat zwei Tests. xUnit führt jeden einzeln aus und schaltet ein Lämpchen daneben ein.
  2. Test 1, vorbereiten: ein Preis von 50 € und 10% Rabatt.
  3. Ausführen: Discount.Apply(50, 10) gibt 45 zurück.
  4. Prüfen: Assert.Equal vergleicht den erwarteten Wert 45 mit dem zurückgegebenen 45. Sie sind gleich, also besteht der Test.
  5. Test 2: genau 100 € und 10%. Wir erwarten 85, weil es ab 100 € noch 5% gibt.
  6. Die Bedingung in der Methode ist price > 100, und 100 ist nicht größer als 100. Die Methode gibt 90 zurück.
  7. Erwartet 85, zurückgegeben 90: Der Test schlägt fehl und sagt genau das.
  8. Du änderst die Bedingung in price >= 100 und führst die Tests noch einmal aus.
  9. Jetzt erfüllt 100 die Bedingung, und die Methode gibt 85 zurück.
  10. Erwartet und zurückgegeben stimmen überein. Beide Tests sind grün.
Genauer, für Neugierige
  • In einem echten Projekt stehen die Tests in einem eigenen Projekt neben dem Code, zum Beispiel Shop.Tests neben Shop. Visual Studio und Rider zeigen alle Tests im Test Explorer: Du klickst auf „Run“ und siehst das Ergebnis.
Struktur
Shop/Discount.cs der Code
Shop/Shop.csproj
Shop.Tests/DiscountTest.cs die Tests dazu
Shop.Tests/Shop.Tests.csproj hier wird xUnit hinzugefügt
  • Das Testprojekt erstellst du mit dotnet new xunit, und dotnet test baut es und führt die Tests aus:
Befehle
dotnet new xunit -o Shop.Tests ein neues Testprojekt
dotnet add Shop.Tests reference Shop die Tests sehen den Code
dotnet test baut und führt alle Tests aus
dotnet test --filter "FullyQualifiedName~Discount"
nur die Tests mit Discount im Namen
  • xUnit ist ein Paket in der .csproj-Datei des Testprojekts. Die Vorlage fügt die Pakete hinzu, und dotnet add reference fügt die Verbindung zum Code hinzu:
Shop.Tests.csproj
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../Shop/Shop.csproj" />
</ItemGroup>
  • Hier auf der Seite stehen die Tests und Main in einem Programm. Main führt die Tests über die Programmierschnittstelle von xUnit aus (AssemblyRunner aus dem Paket xunit.runner.utility), einen nach dem anderen und mit dem Namen der Methode ohne die Klasse, und gibt die Ergebnisse nach Namen sortiert aus. Sie sind sortiert, weil xUnit keine Reihenfolge der Tests verspricht. Die Fehlermeldungen sind die von xUnit selbst, eingerückt unter dem Namen des Tests:
Der Test-Runner
using System.Reflection;
using Xunit;
using Xunit.Runners;
class Program
{
static void Main()
{
RunTests();
}
// Führt die Tests dieses Programms nacheinander aus und gibt die Ergebnisse nach Namen sortiert aus.
static void RunTests(string category = null)
{
var results = new List<string>();
int failed = 0;
using var runner = AssemblyRunner.WithoutAppDomain(Assembly.GetExecutingAssembly().Location);
if (category != null)
{
runner.TestCaseFilter = test => test.Traits.TryGetValue("Category", out var values) && values.Contains(category);
}
runner.OnTestPassed = test => results.Add("OK " + test.TestDisplayName);
runner.OnTestFailed = test =>
{
failed++;
results.Add("FEHLER " + test.TestDisplayName + "\n " + test.ExceptionMessage.ReplaceLineEndings("\n "));
};
runner.Start(new AssemblyRunnerStartOptions { Parallel = false, MethodDisplay = TestMethodDisplay.Method });
while (runner.Status != AssemblyRunnerStatus.Idle)
{
Thread.Sleep(10);
}
results.Sort(string.CompareOrdinal);
foreach (string result in results)
{
Console.WriteLine(result);
}
Console.WriteLine("Tests: " + results.Count + ", bestanden: " + (results.Count - failed) + ", fehlgeschlagen: " + failed);
}
}

Probier es aus

Mehrwertsteuer mit double

Der Preis mit Mehrwertsteuer ist der Preis mal 1,2. Zwei Tests:

Was zeigen die Tests?

class Vat
{
public static double WithVat(double price)
{
return price * 1.2;
}
}
using Xunit;
public class VatTest
{
[Fact]
public void Coffee()
{
Assert.Equal(12.0, Vat.WithVat(10));
}
[Fact]
public void Tea()
{
Assert.Equal(3.6, Vat.WithVat(3));
}
}
Antwort

Die Idee

Ja oder nein, und eine erwartete Exception

Neben Gleichheit prüfst du oft eine Bedingung oder dass der Code ablehnt. Hier ist Account aus dem Modul über Exceptions, mit einer neuen Methode CanPay:

Account.cs
class InsufficientFundsException : Exception
{
public InsufficientFundsException(int shortfall) : base("Es fehlen " + shortfall + " €")
{
}
}
class Account
{
public int Balance { get; private set; }
public Account(int balance)
{
Balance = balance;
}
public bool CanPay(int amount)
{
return amount <= Balance;
}
public void Withdraw(int amount)
{
if (amount <= 0)
{
throw new ArgumentException("Der Betrag muss positiv sein");
}
if (amount > Balance)
{
throw new InsufficientFundsException(amount - Balance);
}
Balance -= amount;
}
}
AccountTest.cs
using Xunit;
public class AccountTest
{
[Fact]
public void CanPayWhatIsThere()
{
var account = new Account(100);
Assert.True(account.CanPay(100));
Assert.False(account.CanPay(101));
}
[Fact]
public void WithdrawTooMuch()
{
var account = new Account(100);
var e = Assert.Throws<InsufficientFundsException>(() => account.Withdraw(130));
Assert.Equal("Es fehlen 30 €", e.Message);
Assert.Equal(100, account.Balance);
}
}
Ausgabe
OK CanPayWhatIsThere
OK WithdrawTooMuch
Tests: 2, bestanden: 2, fehlgeschlagen: 0
  • Assert.True und Assert.False prüfen eine Bedingung.
  • Assert.Throws<Art>(Code) schlägt fehl, wenn der Code keine Exception dieser Art wirft. Die Art steht in < >, wie in List<string>. Den Code übergibst du als Lambda: Assert.Throws führt ihn selbst aus und fängt die Exception. Dann gibt es sie zurück, damit du auch die Meldung prüfen kannst (e.Message).

Der zweite Test prüft auch etwas, das man leicht vergisst: Nach der Ablehnung ist der Kontostand derselbe.

Assert.Throws<T> will eine Exception genau der Art T, nicht einer Unterklasse davon. InsufficientFundsException erbt von Exception, aber Assert.Throws<Exception> schlägt fehl. Für „diese Art oder eine Unterklasse davon“ gibt es Assert.ThrowsAny<T>:

AccountTest.cs
public class AccountTest
{
[Fact]
public void ExactType()
{
var account = new Account(100);
Assert.Throws<Exception>(() => account.Withdraw(130));
}
[Fact]
public void TypeOrDerived()
{
var account = new Account(100);
Assert.ThrowsAny<Exception>(() => account.Withdraw(130));
}
}
Ausgabe
FEHLER ExactType
Assert.Throws() Failure: Exception type was not an exact match
Expected: typeof(System.Exception)
Actual: typeof(InsufficientFundsException)
OK TypeOrDerived
Tests: 2, bestanden: 1, fehlgeschlagen: 1

Die Idee

Jeder Test beginnt sauber

Hier teilen sich die beiden Tests ein Konto mit 100 €:

AccountTest.cs
public class AccountTest
{
private static Account account = new Account(100);
[Fact(DisplayName = "40 € abheben lässt 60 € übrig")]
public void Withdraw40()
{
account.Withdraw(40);
Assert.Equal(60, account.Balance);
}
[Fact(DisplayName = "70 € abheben lässt 30 € übrig")]
public void Withdraw70()
{
account.Withdraw(70);
Assert.Equal(30, account.Balance);
}
}
Ausgabe
FEHLER 70 € abheben lässt 30 € übrig
Es fehlen 10 €
OK 40 € abheben lässt 60 € übrig
Tests: 2, bestanden: 1, fehlgeschlagen: 1

account ist ein static-Feld: ein Konto für alle Tests. xUnit verspricht keine Reihenfolge der Tests, und hier hat es zuerst den Test für 40 € ausgeführt. Beim zweiten sind noch 60 € übrig, und er hebt 70 € ab. Wäre die Reihenfolge umgekehrt, würde der andere fehlschlagen. Ein Test, der von einem anderen Test abhängt, ist eine Falle. xUnit hat kein eigenes Attribut für die Vorbereitung: Für jeden Test erstellt es ein neues Objekt der Testklasse. Deshalb ist das Konto ein normales Feld, und es wird im Konstruktor erstellt:

AccountTest.cs
using Xunit;
public class AccountTest
{
private readonly Account account;
public AccountTest()
{
account = new Account(100);
}
[Fact(DisplayName = "40 € abheben lässt 60 € übrig")]
public void Withdraw40()
{
account.Withdraw(40);
Assert.Equal(60, account.Balance);
}
[Fact(DisplayName = "70 € abheben lässt 30 € übrig")]
public void Withdraw70()
{
account.Withdraw(70);
Assert.Equal(30, account.Balance);
}
}
Ausgabe
OK 40 € abheben lässt 60 € übrig
OK 70 € abheben lässt 30 € übrig
Tests: 2, bestanden: 2, fehlgeschlagen: 0
Der Konstruktor wird vor jedem Test aufgerufen, weil jeder Test ein neues Objekt bekommt. DisplayName in [Fact] gibt dem Test einen Namen in Worten. Er wird im Ergebnis statt des Methodennamens angezeigt.
Genauer, für Neugierige
Weitere Dinge aus xUnit, denen du begegnen wirst:
xUnit
Konstruktor vor jedem Test (ein neues Objekt für jeden Test)
Dispose() aus IDisposable nach jedem Test (Aufräumen)
IClassFixture<T> ein gemeinsames Objekt T für alle Tests der Klasse
[Fact(Skip = "...")] der Test wird vorerst übersprungen
[Trait("Category", "fast")] ein Etikett, damit du nur einen Teil der Tests ausführst
[MemberData(nameof(Prices))] die Zeilen kommen aus einer static-Property oder -Methode
[ClassData(typeof(PriceRows))] die Zeilen kommen aus einer eigenen Klasse
[Collection("Datenbank")] Klassen in einer Collection laufen nicht gleichzeitig
(xUnit führt verschiedene Testklassen parallel aus)
ITestOutputHelper Ausgabe aus dem Test, die im Ergebnis zu sehen ist

Die Idee

Eine Prüfung, viele Fälle

Vier Fälle für den Rabatt bedeuten vier fast gleiche Methoden. [Theory] ist ein Test mit Parametern, und jedes [InlineData] ist eine Zeile „Preis, Prozent, erwartet“, mit der xUnit die Methode ausführt:

DiscountTest.cs
using Xunit;
public class DiscountTest
{
[Theory]
[InlineData(50, 10, 45.0)]
[InlineData(99, 0, 99.0)]
[InlineData(100, 10, 85.0)]
[InlineData(200, 20, 150.0)]
public void Apply(double price, int percent, double expected)
{
Assert.Equal(expected, Discount.Apply(price, percent));
}
}
Ausgabe
OK Apply(price: 100, percent: 10, expected: 85)
OK Apply(price: 200, percent: 20, expected: 150)
OK Apply(price: 50, percent: 10, expected: 45)
OK Apply(price: 99, percent: 0, expected: 99)
Tests: 4, bestanden: 4, fehlgeschlagen: 0

Jede Zeile ist ein eigener Test mit einem eigenen Ergebnis. xUnit bildet seinen Namen aus der Methode und den Werten der Parameter. Ein neuer Fall ist eine neue Zeile, keine neue Methode. Die wichtigen Fälle liegen an den Grenzen: genau 100 €, 99 €, 0%.

Probier es aus

Finde den Fehler anhand des Tests

Die Lieferung kostet 4 €, ab 50 € ist sie kostenlos. Der Test:

DeliveryTest.cs
public class DeliveryTest
{
[Theory]
[InlineData(20, 4)]
[InlineData(50, 0)]
[InlineData(80, 0)]
public void Price(int total, int expected)
{
Assert.Equal(expected, Delivery.Price(total));
}
}
Ausgabe
FEHLER Price(total: 50, expected: 0)
Assert.Equal() Failure: Values differ
Expected: 0
Actual: 4
OK Price(total: 20, expected: 4)
OK Price(total: 80, expected: 0)
Tests: 3, bestanden: 2, fehlgeschlagen: 1

In welcher Zeile des Codes ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Zuerst der Test

Die Kasse soll den Gutscheincode HERBST10 für 10% Rabatt annehmen, egal ob er klein oder groß geschrieben ist. Diesmal schreibst du zuerst die Tests:

PromoTest.cs
public class PromoTest
{
[Fact]
public void KnownCode()
{
Assert.Equal(10, Promo.Percent("HERBST10"));
}
[Fact]
public void CodeIsCaseInsensitive()
{
Assert.Equal(10, Promo.Percent("herbst10"));
}
[Fact]
public void UnknownCode()
{
Assert.Equal(0, Promo.Percent("KEINER"));
}
}
  1. Rot. Vorerst gibt die Methode 0 zurück. Die Tests schlagen fehl, und das ist gut: Es bedeutet, dass sie wirklich etwas prüfen.
class Promo
{
public static int Percent(string code)
{
return 0;
}
}
Ausgabe
FEHLER CodeIsCaseInsensitive
Assert.Equal() Failure: Values differ
Expected: 10
Actual: 0
FEHLER KnownCode
Assert.Equal() Failure: Values differ
Expected: 10
Actual: 0
OK UnknownCode
Tests: 3, bestanden: 1, fehlgeschlagen: 2
  1. Grün. Du schreibst den einfachsten Code, mit dem sie bestehen:
class Promo
{
public static int Percent(string code)
{
if (code.ToUpper() == "HERBST10")
{
return 10;
}
return 0;
}
}
Ausgabe
OK CodeIsCaseInsensitive
OK KnownCode
OK UnknownCode
Tests: 3, bestanden: 3, fehlgeschlagen: 0
  1. Aufräumen. Jetzt kannst du die Codes in ein Dictionary verschieben, neue hinzufügen und die Methode aufräumen. Die Tests sagen dir, wenn du etwas kaputt machst.

Das ist TDD (test-driven development, testgetriebene Entwicklung). Wenn du den Test zuerst schreibst, musst du entscheiden, was genau die Methode tun soll, bevor du darüber nachdenkst, wie.

Genauer

Testgruppen und Ersatzobjekte

xUnit hat keine Suites. Eine Gruppe von Tests ist ein Etikett ([Trait]) an den Klassen, und die Gruppe auszuführen ist ein Filter. Die Tests der Lieferung mit dem Etikett „Kasse“:

DeliveryTest.cs
[Trait("Category", "Kasse")]
public class DeliveryTest
{
[Theory]
[InlineData(20, 4)]
[InlineData(50, 0)]
[InlineData(80, 0)]
public void Price(int total, int expected)
{
Assert.Equal(expected, Delivery.Price(total));
}
}

DiscountTest und AccountTest haben dasselbe Etikett. Der Befehl führt nur die Tests mit dem Etikett aus. Die Ausgabe stammt vom Test-Runner der Seite, der nach demselben Etikett filtert (TestCaseFilter). VatTest aus der Karte zur Mehrwertsteuer steht im selben Programm, hat aber kein Etikett und wird nicht ausgeführt:

dotnet test --filter "Category=Kasse"
Ausgabe
OK 40 € abheben lässt 60 € übrig
OK 70 € abheben lässt 30 € übrig
OK ExtraFivePercentFrom100
OK Price(total: 20, expected: 4)
OK Price(total: 50, expected: 0)
OK Price(total: 80, expected: 0)
OK TenPercentOff
Tests: 7, bestanden: 7, fehlgeschlagen: 0
Genauer, für Neugierige
  • Manchmal hängt der Code von etwas ab, das du in einem Test nicht aufrufen willst: ein Wechselkurs von einer Bank, eine Zahlung, eine E-Mail. Dann übergibst du ein Ersatzobjekt (Mock), das zurückgibt, was du ihm sagst. Die bekannteste Bibliothek dafür in .NET ist Moq:
PriceInUsdTest.cs
using Moq;
using Xunit;
public interface IExchangeRates
{
double EurTo(string currency);
}
class Converter
{
private readonly IExchangeRates rates;
public Converter(IExchangeRates rates)
{
this.rates = rates;
}
public double ToUsd(double eur)
{
return eur * rates.EurTo("USD");
}
}
public class PriceInUsdTest
{
[Fact]
public void ConvertsWithTodaysRate()
{
var rates = new Mock<IExchangeRates>();
rates.Setup(r => r.EurTo("USD")).Returns(1.10);
var converter = new Converter(rates.Object);
Assert.Equal(11.0, converter.ToUsd(10), 0.001);
rates.Verify(r => r.EurTo("USD"), Times.Once());
}
}
Ausgabe
OK ConvertsWithTodaysRate
Tests: 1, bestanden: 1, fehlgeschlagen: 0

new Mock<IExchangeRates>() erstellt ein Objekt, das das Interface implementiert, Setup(...).Returns(...) sagt, was es zurückgeben soll, und Verify prüft, dass die Methode genau einmal aufgerufen wurde (Times.Once()). rates.Object ist das Ersatzobjekt selbst, das du an Converter übergibst. Das Interface ist public, weil Moq selbst eine Klasse erstellt, die es implementiert. Hier auf der Seite wird der Test wirklich ausgeführt.

  • Weitere Assertions neben Assert.Equal, Assert.True und Assert.Throws:
Assertions
Assert.NotEqual(a, b) sind sie verschieden
Assert.Same(a, b) ist es dasselbe Objekt, nicht nur gleich
Assert.NotSame(a, b)
Assert.Null(x), Assert.NotNull(x)
Assert.Equal(list1, list2) zwei Listen (oder Arrays) mit denselben Elementen in derselben Reihenfolge
Assert.Contains(x, items), Assert.DoesNotContain(x, items)
Assert.Empty(items), Assert.Single(items)
Assert.InRange(x, low, high) x liegt zwischen low und high
Assert.Matches(pattern, text) der Text passt zu einem regulären Ausdruck
Assert.IsType<T>(x) das Objekt ist genau vom Typ T
Assert.ThrowsAny<T>(() => ...) eine Exception vom Typ T oder einer Unterklasse davon
Assert.Fail("Meldung") der Test schlägt sofort fehl

Übung

Schreib die Tests

Löse die Aufgaben in deinem Editor, mit xUnit in einem Testprojekt (dotnet new xunit).

1. Der Rabattrechner. Schreib DiscountTest für Discount.Apply aus der ersten Karte. Deck diese Fälle ab: 80 € mit 10% Rabatt sind 72 €, 100 € mit 0% sind 95 €, 150 € mit 20% sind 112,50 €, 0 € mit 30% sind 0 €, und genau 100 € mit 10% sind 85 €. Führ sie auch gegen die kaputte Version mit price > 100 aus. Genau die beiden Fälle mit 100 € sollen fehlschlagen:

Ergebnis gegen die kaputte Version
FEHLER Apply(price: 100, percent: 0, expected: 95)
Assert.Equal() Failure: Values are not within tolerance 0.001
Expected: 95
Actual: 100
FEHLER Exactly100GetsTheExtra
Assert.Equal() Failure: Values are not within tolerance 0.001
Expected: 85
Actual: 90
OK Apply(price: 0, percent: 30, expected: 0)
OK Apply(price: 150, percent: 20, expected: 112.5)
OK Apply(price: 80, percent: 10, expected: 72)
Tests: 5, bestanden: 3, fehlgeschlagen: 2

2. Das Konto. Schreib AccountTest für Account aus der Karte „Ja oder nein, und eine erwartete Exception“. Erstelle das Konto mit 100 € in AccountTest(). Prüfe, dass:

  • 30 € abheben 70 € übrig lässt;
  • du alles abheben kannst und 0 übrig bleibt;
  • 150 € abheben eine InsufficientFundsException mit der Meldung „Es fehlen 50 €“ wirft und der Kontostand 100 € bleibt;
  • 0 und -20 € eine ArgumentException werfen (eine [Theory] mit [InlineData(0)] und [InlineData(-20)]).

3. Der Fehler im Notendurchschnitt. Die Methode sieht richtig aus:

Grades.cs
class Grades
{
public static double Average(int[] grades)
{
int sum = 0;
foreach (int g in grades)
{
sum += g;
}
return sum / grades.Length;
}
}
Schreib zwei Tests: Der Durchschnitt von 4 und 6 ist 5,0, und der Durchschnitt von 5 und 6 ist 5,5 (mit einer erlaubten Abweichung von 0,001). Einer davon schlägt fehl:
Ergebnis
FEHLER AverageWithAFraction
Assert.Equal() Failure: Values are not within tolerance 0.001
Expected: 5.5
Actual: 5
OK WholeAverage
Tests: 2, bestanden: 1, fehlgeschlagen: 1

Finde die Ursache und korrigiere die Methode, damit beide bestehen.

Test

Teste dich

1. Was prüft ein Unit-Test?
2. Der Test schlägt fehl mit „Expected: 85“ und „Actual: 90“. Was hat die Methode zurückgegeben?
3. Warum wird das Konto im Konstruktor der Testklasse erstellt und nicht einmal in einem static-Feld?
4. Wie prüfst du, dass Withdraw(130) eine InsufficientFundsException wirft?
5. Was ist die Reihenfolge bei TDD?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·