Skip to content

Commit 33a96b2

Browse files
committed
Revise L19
1 parent ee90b05 commit 33a96b2

18 files changed

Lines changed: 543 additions & 606 deletions

File tree

19_Testen.md

Lines changed: 218 additions & 90 deletions
Large diffs are not rendered by default.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
bin/
2+
obj/
3+
.coverage/
4+
.coverage-report/
5+
TestResults/
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Black-Box-Testing — Live-Übung
2+
3+
Begleitübung zum Foliensatz 19 (Abschnitt *Black-Box-Testing / Spezifikationsorientiert*).
4+
Die Studierenden leiten Testfälle **allein aus der Spezifikation** ab —
5+
ohne den Quelltext des Übungsobjekts zu kennen.
6+
7+
## Übungsobjekt
8+
9+
`RabattService/Rabatt.cs` berechnet einen Mengenrabatt nach Bestellwert:
10+
11+
| Bestellwert (Euro) | Rabatt |
12+
| ------------------ | ------ |
13+
| 0,00 – 99,99 | 0 % |
14+
| 100,00 – 499,99 | 5 % |
15+
| 500,00 – 1999,99 | 10 % |
16+
| ab 2000,00 | 15 % |
17+
| negativer Wert | `ArgumentOutOfRangeException` |
18+
19+
In der Implementierung steckt ein **Off-by-one-Fehler an der 2000-Grenze**
20+
(`<= 2000m` statt `< 2000m`). Er ist *nicht* über die Äquivalenzklassen-
21+
Repräsentanten sichtbar, sondern **nur** über die **Grenzwertanalyse** — genau
22+
das ist die Pointe der Übung.
23+
24+
## Ablauf in der Vorlesung
25+
26+
1. **Spezifikation zeigen** (Tabelle oben), Quelltext geschlossen lassen.
27+
2. Gemeinsam **Äquivalenzklassen** bilden (4 gültige + 1 ungültige) und die
28+
`[InlineData]`-Zeilen in `Rabatt_je_Aequivalenzklasse` ausfüllen.
29+
→ alle grün. *„Sind wir fertig?“*
30+
3. **Grenzwertanalyse** ergänzen (`Rabatt_an_den_Grenzen`): je Grenze die zwei
31+
benachbarten Werte. Beim Wert `2000.00` wird der Test **rot**.
32+
4. Erst **jetzt** den Quelltext öffnen → der Black-Box-Test hat den Fehler
33+
gefunden, ohne die Interna zu kennen.
34+
35+
## Befehle
36+
37+
```bash
38+
# Tests ausführen (Aufgabenstand)
39+
dotnet test
40+
```
41+
42+
Lösung vorführen:
43+
44+
```bash
45+
# Musterlösung aktivieren, Aufgabe vorübergehend ausblenden
46+
mv RabattService.Tests/Aufgabe_BlackBox.cs RabattService.Tests/Aufgabe_BlackBox.cs.txt
47+
mv RabattService.Tests/Loesung_BlackBox.cs.txt RabattService.Tests/Loesung_BlackBox.cs
48+
dotnet test # -> 1 Test schlägt fehl: (2000.00 -> 15 %)
49+
50+
# danach zurücksetzen
51+
mv RabattService.Tests/Loesung_BlackBox.cs RabattService.Tests/Loesung_BlackBox.cs.txt
52+
mv RabattService.Tests/Aufgabe_BlackBox.cs.txt RabattService.Tests/Aufgabe_BlackBox.cs
53+
```
54+
55+
> Bonus: Nach dem Fix (`< 2000m`) sind alle Tests grün — schöner Anknüpfungspunkt
56+
> für „Test schreiben → Fehler reproduzieren → Fix → Test grün“ (vgl. TDD).
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
namespace RabattService.Tests;
2+
3+
using Xunit;
4+
5+
// ===========================================================================
6+
// ÜBUNG: BLACK-BOX-TESTING
7+
// ===========================================================================
8+
//
9+
// Sie kennen NUR die Spezifikation (siehe Aufgabenblatt / Doku in Rabatt.cs):
10+
//
11+
// Bestellwert (Euro) Rabatt
12+
// -----------------------------------
13+
// 0,00 – 99,99 0 %
14+
// 100,00 – 499,99 5 %
15+
// 500,00 – 1999,99 10 %
16+
// ab 2000,00 15 %
17+
//
18+
// negativer Bestellwert -> ArgumentOutOfRangeException
19+
//
20+
// Den Quelltext von Rabatt.RabattInProzent(...) betrachten wir als
21+
// Black Box - wir testen ausschließlich gegen die Tabelle oben.
22+
//
23+
// Vorgehen:
24+
// 1) ÄQUIVALENZKLASSEN bilden: je Rabattstufe ein "typischer" Repräsentant
25+
// plus die ungültige Klasse (negativer Wert).
26+
// 2) GRENZWERTANALYSE: an jedem Übergang die beiden direkt benachbarten
27+
// Werte testen (z. B. 99,99 und 100,00).
28+
//
29+
// Führen Sie die Tests mit `dotnet test` aus. Mindestens einer wird
30+
// fehlschlagen - finden Sie heraus, an welcher Klassengrenze der Fehler
31+
// in der Implementierung steckt!
32+
// ===========================================================================
33+
34+
public class Aufgabe_BlackBox
35+
{
36+
// ----- 1. Äquivalenzklassen: ein Repräsentant je Stufe -----------------
37+
38+
[Theory]
39+
[InlineData(50.00, 0)] // Klasse "0 %"
40+
// TODO: je eine Zeile für die Klassen 5 %, 10 % und 15 % ergänzen
41+
// [InlineData( ?, 5)]
42+
// [InlineData( ?, 10)]
43+
// [InlineData( ?, 15)]
44+
public void Rabatt_je_Aequivalenzklasse(decimal bestellwert, int erwartet)
45+
{
46+
var rabatt = Rabatt.RabattInProzent(bestellwert);
47+
Assert.Equal(erwartet, rabatt);
48+
}
49+
50+
// ----- 2. Grenzwertanalyse: Werte direkt an den Übergängen -------------
51+
//
52+
// Tipp: An jeder Grenze g testen Sie den größten Wert der unteren Klasse
53+
// und den kleinsten Wert der oberen Klasse (z. B. 99.99 und 100.00).
54+
55+
[Theory]
56+
[InlineData(99.99, 0)]
57+
[InlineData(100.00, 5)]
58+
// TODO: die Grenzen 500 und 2000 analog ergänzen
59+
// [InlineData( 499.99, 5)]
60+
// [InlineData( 500.00, 10)]
61+
// [InlineData(1999.99, 10)]
62+
// [InlineData(2000.00, 15)]
63+
public void Rabatt_an_den_Grenzen(decimal bestellwert, int erwartet)
64+
{
65+
var rabatt = Rabatt.RabattInProzent(bestellwert);
66+
Assert.Equal(erwartet, rabatt);
67+
}
68+
69+
// ----- 3. Ungültige Eingabe (ungültige Äquivalenzklasse) ---------------
70+
71+
[Fact]
72+
public void NegativerBestellwert_wirftException()
73+
{
74+
// TODO: prüfen, dass eine ArgumentOutOfRangeException geworfen wird.
75+
// Assert.Throws<...>( () => Rabatt.RabattInProzent(-1m) );
76+
}
77+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
namespace RabattService.Tests;
2+
3+
using Xunit;
4+
5+
// ===========================================================================
6+
// MUSTERLÖSUNG zur Black-Box-Übung
7+
// ===========================================================================
8+
//
9+
// Diese Datei trägt bewusst die Endung ".cs.txt", damit sie NICHT mit
10+
// kompiliert wird (sonst kollidieren die [Theory]-Daten in der Vorführung).
11+
//
12+
// Zum Vorführen der Lösung:
13+
// mv Loesung_BlackBox.cs.txt Loesung_BlackBox.cs
14+
// dotnet test
15+
// und ggf. Aufgabe_BlackBox.cs vorübergehend nach .cs.txt umbenennen.
16+
//
17+
// ERGEBNIS: Der Testfall (2000.00 -> 15 %) schlägt fehl. Die Black-Box-
18+
// Grenzwertanalyse deckt damit den Off-by-one-Fehler an der 2000-Grenze auf
19+
// (im Code steht <= 2000m statt < 2000m, deshalb erhält 2000,00 nur 10 %).
20+
// ===========================================================================
21+
22+
public class Loesung_BlackBox
23+
{
24+
// ----- 1. Äquivalenzklassen --------------------------------------------
25+
26+
[Theory]
27+
[InlineData(50.00, 0)] // Klasse 0 %
28+
[InlineData(250.00, 5)] // Klasse 5 %
29+
[InlineData(1000.00, 10)] // Klasse 10 %
30+
[InlineData(5000.00, 15)] // Klasse 15 %
31+
public void Rabatt_je_Aequivalenzklasse(decimal bestellwert, int erwartet)
32+
{
33+
var rabatt = Rabatt.RabattInProzent(bestellwert);
34+
Assert.Equal(erwartet, rabatt);
35+
}
36+
37+
// ----- 2. Grenzwertanalyse ---------------------------------------------
38+
39+
[Theory]
40+
[InlineData(99.99, 0)]
41+
[InlineData(100.00, 5)]
42+
[InlineData(499.99, 5)]
43+
[InlineData(500.00, 10)]
44+
[InlineData(1999.99, 10)]
45+
[InlineData(2000.00, 15)] // <- deckt den Off-by-one-Fehler auf
46+
public void Rabatt_an_den_Grenzen(decimal bestellwert, int erwartet)
47+
{
48+
var rabatt = Rabatt.RabattInProzent(bestellwert);
49+
Assert.Equal(erwartet, rabatt);
50+
}
51+
52+
// ----- 3. Ungültige Eingabe --------------------------------------------
53+
54+
[Fact]
55+
public void NegativerBestellwert_wirftException()
56+
{
57+
Assert.Throws<ArgumentOutOfRangeException>(
58+
() => Rabatt.RabattInProzent(-1m));
59+
}
60+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.4">
12+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
13+
<PrivateAssets>all</PrivateAssets>
14+
</PackageReference>
15+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
16+
<PackageReference Include="xunit" Version="2.9.2" />
17+
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
18+
</ItemGroup>
19+
20+
<ItemGroup>
21+
<Using Include="Xunit" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="..\RabattService\RabattService.csproj" />
26+
</ItemGroup>
27+
28+
</Project>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
namespace RabattService;
2+
3+
/// <summary>
4+
/// Übungsobjekt für Black-Box-Testing.
5+
///
6+
/// Die SPEZIFIKATION (das, was die Studierenden kennen) lautet:
7+
///
8+
/// Berechne den prozentualen Mengenrabatt anhand des Bestellwerts:
9+
///
10+
/// Bestellwert (Euro) Rabatt
11+
/// -----------------------------------
12+
/// 0,00 – 99,99 0 %
13+
/// 100,00 – 499,99 5 %
14+
/// 500,00 – 1999,99 10 %
15+
/// ab 2000,00 15 %
16+
///
17+
/// Ein negativer Bestellwert ist ungültig und führt zu einer
18+
/// ArgumentOutOfRangeException.
19+
///
20+
/// Die INTERNA (der Code hier) kennen die Studierenden beim Black-Box-Test
21+
/// NICHT. Sie leiten ihre Testfälle allein aus der Tabelle oben ab:
22+
/// - eine Äquivalenzklasse je Rabattstufe (+ die ungültige Klasse < 0)
23+
/// - die Grenzwerte an den Übergängen (99,99 / 100,00 ; 499,99 / 500,00 ; ...)
24+
///
25+
/// Achtung: In der Implementierung steckt ein typischer Off-by-one-Fehler
26+
/// an einer Klassengrenze. Genau diesen soll die Grenzwertanalyse aufdecken.
27+
/// </summary>
28+
public static class Rabatt
29+
{
30+
public static int RabattInProzent(decimal bestellwert)
31+
{
32+
if (bestellwert < 0)
33+
throw new ArgumentOutOfRangeException(
34+
nameof(bestellwert), "Der Bestellwert darf nicht negativ sein.");
35+
36+
if (bestellwert < 100m) return 0;
37+
if (bestellwert < 500m) return 5;
38+
if (bestellwert <= 2000m) return 10; // FEHLER: muss < 2000 sein!
39+
// dadurch erhält 2000,00 nur 10 % statt 15 %
40+
return 15;
41+
}
42+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.0.31903.59
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RabattService", "RabattService\RabattService.csproj", "{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}"
7+
EndProject
8+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RabattService.Tests", "RabattService.Tests\RabattService.Tests.csproj", "{A8ADD3E9-524A-4A96-809F-78F47E2942FC}"
9+
EndProject
10+
Global
11+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
12+
Debug|Any CPU = Debug|Any CPU
13+
Debug|x64 = Debug|x64
14+
Debug|x86 = Debug|x86
15+
Release|Any CPU = Release|Any CPU
16+
Release|x64 = Release|x64
17+
Release|x86 = Release|x86
18+
EndGlobalSection
19+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
20+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
22+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Debug|x64.ActiveCfg = Debug|Any CPU
23+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Debug|x64.Build.0 = Debug|Any CPU
24+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Debug|x86.ActiveCfg = Debug|Any CPU
25+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Debug|x86.Build.0 = Debug|Any CPU
26+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Release|Any CPU.Build.0 = Release|Any CPU
28+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Release|x64.ActiveCfg = Release|Any CPU
29+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Release|x64.Build.0 = Release|Any CPU
30+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Release|x86.ActiveCfg = Release|Any CPU
31+
{15BFFAC5-2DCA-4675-AFF2-4660EDF1B1A4}.Release|x86.Build.0 = Release|Any CPU
32+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
34+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Debug|x64.ActiveCfg = Debug|Any CPU
35+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Debug|x64.Build.0 = Debug|Any CPU
36+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Debug|x86.ActiveCfg = Debug|Any CPU
37+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Debug|x86.Build.0 = Debug|Any CPU
38+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
39+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Release|Any CPU.Build.0 = Release|Any CPU
40+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Release|x64.ActiveCfg = Release|Any CPU
41+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Release|x64.Build.0 = Release|Any CPU
42+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Release|x86.ActiveCfg = Release|Any CPU
43+
{A8ADD3E9-524A-4A96-809F-78F47E2942FC}.Release|x86.Build.0 = Release|Any CPU
44+
EndGlobalSection
45+
GlobalSection(SolutionProperties) = preSolution
46+
HideSolutionNode = FALSE
47+
EndGlobalSection
48+
EndGlobal

code/16_Testen/Testen/unit-testing-example/CalcAppPython/.gitignore

Lines changed: 0 additions & 57 deletions
This file was deleted.

0 commit comments

Comments
 (0)