Von Dr. Cassian Holt, Senior Architect & Programming Language Historian bei Java Fleet Systems Consulting in Essen-Rüttenscheid
🔗 Bisher in der Testing-Time-Travel-Serie:
- Das ultimative Testing Glossar
- Teil 1: Die Evolution des Testens – JUnit 3→4→5 Geschichte, Testing-Basics
- Teil 2: Die Wissenschaft der Test-Pyramide – Unit/Integration/E2E als Physik
Heute: Teil 3 – Property-Based Testing & TDD-Kulturschock
📝 Kurze Zusammenfassung
🔬 Property-Based Testing:
- Statt 10 Beispiele → teste ALLE möglichen Inputs
- jqwik generiert tausende Testfälle automatisch
- Mathematische Properties statt konkrete Werte
🧬 TDD = Entwicklungs-Kulturschock:
- Red-Green-Refactor = völlig andere Arbeitsweise
- Tests BEFORE Code = Paradigmen-Wechsel
- Braucht Team-Buy-In und Change Management
- Martin Fowler’s TDD
🎯 Diese Woche: Von Beispiel-Tests zu wissenschaftlichen Properties, dann zur TDD-Revolution!
🌟 Willkommen zurück, Testing-Scientists!
Dr. Cassian hier – und Nova kam heute Morgen zu mir: „Cassian, ich verstehe die Test-Pyramide, aber meine Unit Tests testen immer nur einzelne Beispiele. Gibt es nicht einen besseren Weg?“
Perfekte Frage! Heute lernt ihr die Evolution der Unit Tests und dann eine komplette Entwicklungs-Revolution:
- 🔬 Property-Based Testing – Von Einzelfällen zu mathematischen Gesetzen
- 🧬 TDD-Kulturschock – Eine völlig andere Art zu entwickeln
Wie Amos Burton sagen würde: „Time to level up your testing game!“ ⚙️
🔬 Property-Based Testing: Hari Seldon für Code
Das Problem mit Example-Based Testing:
Nova’s bisheriger Ansatz (aus Teil 2):
@Test void shouldSortList() { // Nur EIN Beispiel List<Integer> input = Arrays.asList(3, 1, 2); List<Integer> expected = Arrays.asList(1, 2, 3); assertThat(sort(input)).isEqualTo(expected); // Aber was ist mit [100, -5, 0]? // Was ist mit [1]? // Was ist mit []? // Was ist mit 10.000 Elementen? }
Das Problem: Wir testen Beispiele, nicht Gesetzmäßigkeiten! 🤔
Property-Based Testing: Teste ALLE möglichen Fälle!
Mit jqwik (Java Property-Based Testing):
⚠️ Warnung: TDD ist ein Kulturschock!
Bevor wir zur Wissenschaft kommen – ein ehrliches Gespräch: TDD einzuführen ist wie Vegetarier in einer Grillbude zu werden. Es verändert ALLES! 🤯
Nova’s erste TDD-Woche (authentisch):
- Tag 1: „Das ist viel zu langsam!“
- Tag 3: „Ich schreibe mehr Test-Code als echten Code!“
- Tag 5: „Franz-Martin, können wir nicht einfach bei unserer alten Art bleiben?“
- Tag 10: „Oh… mein Code bricht nie mehr!“
🔥 Die TDD-Gruppendynamiken (Real Talk):
Team-Widerstand ist NORMAL:
// Was Entwickler wirklich denken: public class TeamReactions { @Override public String seniorDeveloperReaction() { return "Ich habe 10 Jahre ohne Tests programmiert, warum jetzt ändern?"; } @Override public String juniorDeveloperReaction() { return "Ich verstehe kaum den normalen Code, jetzt auch noch Tests?"; } @Override public String projectManagerReaction() { return "Tests schreiben dauert doppelt so lang! Wir haben Deadlines!"; } @Override public String customerReaction() { return "Mir egal wie ihr testet, ich will Features!"; } }
Die häufigsten TDD-Sabotage-Muster:
- „Zu langsam“ Syndrom: „Wir haben keine Zeit für Tests!“
- „Legacy-Excuse“: „Unser alter Code ist nicht testbar!“
- „Quick-Fix-Mentalität“: „Nur dieser eine Hotfix ohne Test…“
- „Testing-Theater“: Tests nach dem Code schreiben ≠ TDD
🎭 Die TDD-Einführung: Ein Drama in 5 Akten
Akt 1: Euphorie (Woche 1)
// Team nach TDD-Workshop: @Test void shouldCalculateTotal() { // "Das ist ja einfach!" assertThat(calculator.add(2, 3)).isEqualTo(5); }
Akt 2: Frustration (Woche 2-4)
// Team in der Realität: @Test void shouldHandleComplexBusinessLogic() { // "Wie teste ich das? Das hat 15 Dependencies!" // "Ich schreibe schon 3 Stunden an diesem Test!" // "Das ist doch Wahnsinn!" }
Akt 3: Widerstand (Woche 5-8)
// Heimliche Commits ohne Tests: git commit -m "Quick hotfix" // Keine Tests! 😈 git commit -m "Minor change" // Test später... (nie!)
Akt 4: Durchbruch (Woche 8-12)
// Erstes Erfolgserlebnis: @Test void shouldPreventProductionBug() { // Dieser Test hätte den letzten Produktions-Bug verhindert! // Plötzlich: "Oh... Tests sind Versicherungen!" }
Akt 5: Evangelismus (Monat 4+)
// Team wird zu TDD-Evangelisten: public String newTeamMemberOrientation() { return "Bei uns wird ERST der Test geschrieben! IMMER!"; }
Das Geheimnis: TDD IST Evolutionsbiologie! 🦕
Stellt euch vor: Euer Code ist eine Spezies. Tests sind die Umwelt. Nur Code, der alle Tests überlebt, kommt in Production!
🦕 Primitive Code → 🦴 Tests (Extinction Events) → 🦅 Evolved Code
Die Evolution in Aktion – Nova’s TaskApp Beispiel:
Generation 1: Primitive Lebensform (RED Phase)
// Nova's erste Hypothese: "Tasks brauchen nur einen Titel" @Test @DisplayName("🧬 Generation 1: Basic task creation") void generation1_basicTaskCreation() { // HYPOTHESIS: Task braucht mindestens einen Titel Task task = new Task(""); assertThat(task.isValid()).isFalse(); // Leerer Titel = invalid Task validTask = new Task("Learn TDD"); assertThat(validTask.isValid()).isTrue(); // Mit Titel = valid } // Primitive Implementation (GREEN Phase) public class Task { private String title; public Task(String title) { this.title = title; } public boolean isValid() { return title != null && !title.trim().isEmpty(); } }
Was passiert hier? 🤔
- RED: Test schreibt das gewünschte Verhalten auf
- GREEN: Einfachste Implementierung, die Test erfüllt
- Wie ein Einzeller: Basic, aber funktionsfähig!
Generation 2: Umweltdruck (Neue Requirements)
@Test @DisplayName("🧬 Generation 2: Business rules evolution") void generation2_businessRulesEvolution() { Task task = new Task("x"); // Nur 1 Zeichen // Environmental pressure: Business will mindestens 3 Zeichen assertThat(task.isValid()).isFalse(); Task businessTask = new Task("Fix Bug #123"); assertThat(businessTask.isValid()).isTrue(); } // Evolution Schritt 2 (REFACTOR Phase) public class Task { private String title; public Task(String title) { this.title = title; } public boolean isValid() { if (title == null || title.trim().isEmpty()) { return false; } // 🧬 Evolution: Neue Überlebensbedingung! return title.trim().length() >= 3; } }
Generation 3: Massensterben-Event (Security Requirements)
@Test @DisplayName("🧬 Generation 3: Security extinction event") void generation3_securityExtinction() { // Umwelt wird feindlicher: XSS-Schutz erforderlich! Task maliciousTask = new Task("<script>alert('hack')</script>"); assertThat(maliciousTask.isValid()).isFalse(); // Nur sichere Tasks überleben Task safeTask = new Task("Safe Business Task"); assertThat(safeTask.isValid()).isTrue(); } // 🧬 Überlebens-Mutation (Final Evolution) public class Task { private String title; private static final Pattern UNSAFE_PATTERN = Pattern.compile(".*[<>\"'&].*"); public Task(String title) { this.title = title; } public boolean isValid() { if (title == null || title.trim().isEmpty()) { return false; } if (title.trim().length() < 3) { return false; } // 🧬 Neue DNA: XSS-Resistenz! return !UNSAFE_PATTERN.matcher(title).matches(); } }
🎯 Die TDD-Evolution visualisiert:
Umwelt-Druck Code-Evolution ↓ ↓ 📝 Leerer Titel → 🦕 Basic String Check 📝 Min. 3 Zeichen → 🐸 Length Validation 📝 XSS-Schutz → 🦅 Security Pattern Matching 📝 Database Ready → 🚀 Enterprise Solution
Wie die Protomolecule in The Expanse: Code adaptiert sich automatisch an neue Requirements! Ihr müsst nur die „Umweltbedingungen“ (Tests) definieren! 🔬
💡 Praktische TDD-Einführungsstrategien (Franz-Martin’s Playbook):
🎯 Level 1: Sanfte Einführung (Woche 1-2)
// Startet mit simpelsten Fällen: @Test void shouldCreateUser() { User user = new User("nova@example.com"); assertThat(user.getEmail()).isEqualTo("nova@example.com"); // Nicht überwältigend, aber TDD-Gefühl! }
🎯 Level 2: Bug-Fix-TDD (Woche 3-4)
// Jeder Bug-Fix braucht erst einen Test: @Test @DisplayName("Reproduces Bug #1234: NPE when user has no email") void shouldHandleMissingEmail() { // Test schreibt das Problem auf assertThatCode(() -> new User(null)) .doesNotThrowAnyException(); // Dann Bug fixen! }
🎯 Level 3: Feature-TDD (Monat 2)
// Neue Features NUR mit TDD: @Test @DisplayName("New Feature: User can update profile") void shouldUpdateUserProfile() { // Definiert Feature BEVOR Implementierung User user = new User("old@example.com"); user.updateProfile("new@example.com", "New Name"); assertThat(user.getEmail()).isEqualTo("new@example.com"); assertThat(user.getName()).isEqualTo("New Name"); }
🎯 Level 4: Refactoring-Safety (Monat 3)
// Tests als Safety Net für Legacy-Refactoring: @Test @DisplayName("Legacy behavior should remain unchanged") void shouldMaintainLegacyBehavior() { // Charakterisierungstests für Legacy-Code LegacyService service = new LegacyService(); // Dokumentiere IST-Zustand vor Refactoring assertThat(service.processData("input")).isEqualTo("expected_legacy_output"); }
🚨 TDD-Fallstricke (und wie man sie vermeidet):
Fallstrick #1: „Over-Engineering“ der Tests
// ❌ SCHLECHT: Test ist komplizierter als der Code @Test void shouldCalculateTotal() { // 50 Zeilen Setup für 1 Zeile Production-Code MockWebServer server = new MockWebServer(); // ... komplexes Mock-Setup // Der Test ist das Problem! } // ✅ GUT: Test ist einfacher als der Code @Test void shouldCalculateTotal() { assertThat(calculator.add(2, 3)).isEqualTo(5); }
Fallstrick #2: „Testing-Theater“ statt TDD
// ❌ SCHLECHT: Code zuerst, Test nachher public class Calculator { public int add(int a, int b) { return a + b; // Code fertig } } @Test // Nachher geschrieben = nicht TDD! void shouldAdd() { assertThat(new Calculator().add(2, 3)).isEqualTo(5); } // ✅ GUT: Test zuerst (RED), dann Code (GREEN) @Test void shouldAdd() { // ZUERST! assertThat(calculator.add(2, 3)).isEqualTo(5); // Fails! } // Dann Implementation...
🎯 TDD-Einführung: Der Cassian-Plan für Teams
Phase 1: Einzelkämpfer (1-2 Wochen)
- Ein Team-Mitglied macht TDD für kleine Features
- Zeigt Vorteile ohne Team-Zwang
- „Proof of Concept“ mit messbaren Ergebnissen
Phase 2: Pairing (Woche 3-6)
- TDD-Champion macht Pair-Programming mit anderen
- Hands-On Learning, kein theoretischer Workshop
- Wissen multipliziert sich organisch
Phase 3: Team-Adoption (Monat 2-3)
- Neue Features nur noch mit TDD
- Bug-Fixes brauchen reproduzierenden Test
- Code-Reviews prüfen Test-First-Approach
Phase 4: Kultur-Wandel (Monat 4+)
- TDD ist Standard, keine Diskussion mehr
- Neue Team-Mitglieder lernen TDD automatisch
- Team wird selbst zu TDD-Evangelisten
Warum TDD trotz Kulturschock so mächtig ist:
1. Code entwickelt sich automatisch:
// Statt "Was könnte schiefgehen?" zu raten: // → Schreibt Tests für gewünschtes Verhalten // → Code evolviert zur optimalen Lösung
2. Refactoring wird sicher:
// Mit Tests = Safety Net // Ohne Tests = "Hope and pray" Programming
3. Design emerges naturally:
// TDD zwingt zu testbarem Code = bessere Architektur // "If it's hard to test, it's probably bad design"
🔬 Property-Based Testing: Hari Seldon für Code
Das Problem mit Example-Based Testing:
Herkömmlich (Nova’s alter Ansatz):
@Test void shouldSortList() { // Nur EIN Beispiel List<Integer> input = Arrays.asList(3, 1, 2); List<Integer> expected = Arrays.asList(1, 2, 3); assertThat(sort(input)).isEqualTo(expected); // Aber was ist mit [100, -5, 0]? // Was ist mit [1]? // Was ist mit []? // Was ist mit 10.000 Elementen? }
Das Problem: Wir testen Beispiele, nicht Gesetzmäßigkeiten! 🤔
Property-Based Testing: Teste ALLE möglichen Fälle!
Mit jqwik (Java Property-Based Testing):
// Maven Dependency <dependency> <groupId>net.jqwik</groupId> <artifactId>jqwik</artifactId> <version>1.7.4</version> <scope>test</scope> </dependency> @Property @DisplayName("🔬 Sort: Output should always be ordered") void sortOutputAlwaysOrdered(@ForAll List<Integer> randomList) { List<Integer> sorted = sort(randomList); // Property: Sortierte Liste ist immer aufsteigend for (int i = 0; i < sorted.size() - 1; i++) { assertThat(sorted.get(i)).isLessThanOrEqualTo(sorted.get(i + 1)); } } @Property @DisplayName("🔬 Sort: Contains same elements as input") void sortContainsSameElements(@ForAll List<Integer> randomList) { List<Integer> sorted = sort(randomList); // Property: Keine Elemente gehen verloren oder kommen hinzu assertThat(sorted).hasSameSizeAs(randomList); assertThat(sorted).containsExactlyInAnyOrderElementsOf(randomList); } @Property @DisplayName("🔬 Sort: Idempotent (sorting twice = sorting once)") void sortIsIdempotent(@ForAll List<Integer> randomList) { List<Integer> sortedOnce = sort(randomList); List<Integer> sortedTwice = sort(sortedOnce); // Property: Doppelt sortieren ändert nichts assertThat(sortedTwice).isEqualTo(sortedOnce); }
Was passiert hier magically? ✨
jqwik generiert automatisch:
- Leere Listen:
[]
- Ein-Element-Listen:
[42]
- Große Listen:
[1, -999, 847, ...]
mit 1000+ Elementen - Negative Zahlen:
[-1, -5, -999999]
- Duplikate:
[1, 1, 1, 2, 2]
- Edge Cases:
[Integer.MAX_VALUE, Integer.MIN_VALUE]
100 Tests werden zu 10.000 Tests! 🤯
Property-Based Testing für Nova’s TaskApp:
@Property @DisplayName("🔬 Task: Valid tasks should always stay valid after operations") void validTasksStayValidAfterOperations(@ForAll("validTasks") Task task) { // Property: Valid Task bleibt valid nach allen Operations assumeThat(task.isValid()).isTrue(); task.setDescription("Added description"); assertThat(task.isValid()).isTrue(); task.setPriority(Priority.HIGH); assertThat(task.isValid()).isTrue(); task.complete(); assertThat(task.isValid()).isTrue(); // Invariant: Valid Tasks bleiben valid! } @Property @DisplayName("🔬 Task: Title trimming should be consistent") void titleTrimmingConsistent(@ForAll String randomTitle) { Task task = new Task(" " + randomTitle + " "); if (task.isValid()) { // Property: Valid Task hat nie leading/trailing spaces assertThat(task.getTitle()).doesNotStartWith(" "); assertThat(task.getTitle()).doesNotEndWith(" "); } } @Provide Arbitrary<Task> validTasks() { return Arbitraries.strings() .withCharRange('a', 'z') .withCharRange('A', 'Z') .withCharRange('0', '9') .withChars(' ', '-', '_') .ofMinLength(3) .ofMaxLength(100) .map(title -> new Task(title.trim())) .filter(Task::isValid); }
🎯 Example vs Property Testing Vergleich:
Example-Based | Property-Based |
---|---|
10 handpicked cases | 1000+ generated cases |
„Works for my examples“ | „Works mathematically“ |
Misses edge cases | Finds edge cases automatically |
Tests behavior | Tests invariants |
sort([1,2,3]) = [1,2,3] | ∀ lists: sort(list) is ordered |
Wie Hari Seldon’s Psychohistory: Statt einzelne Events vorherzusagen, beweisen wir mathematische Gesetze über alle möglichen Zustände! 📊
Property-Based Testing für Nova’s TaskApp:
@Property @DisplayName("🔬 Task: Valid tasks should always stay valid after operations") void validTasksStayValidAfterOperations(@ForAll("validTasks") Task task) { // Property: Valid Task bleibt valid nach allen Operations assumeThat(task.isValid()).isTrue(); task.setDescription("Added description"); assertThat(task.isValid()).isTrue(); task.setPriority(Priority.HIGH); assertThat(task.isValid()).isTrue(); task.complete(); assertThat(task.isValid()).isTrue(); // Invariant: Valid Tasks bleiben valid! } @Property @DisplayName("🔬 Task: Title trimming should be consistent") void titleTrimmingConsistent(@ForAll String randomTitle) { Task task = new Task(" " + randomTitle + " "); if (task.isValid()) { // Property: Valid Task hat nie leading/trailing spaces assertThat(task.getTitle()).doesNotStartWith(" "); assertThat(task.getTitle()).doesNotEndWith(" "); } } @Provide Arbitrary<Task> validTasks() { return Arbitraries.strings() .withCharRange('a', 'z') .withCharRange('A', 'Z') .withCharRange('0', '9') .withChars(' ', '-', '_') .ofMinLength(3) .ofMaxLength(100) .map(title -> new Task(title.trim())) .filter(Task::isValid); }
🎯 Example vs Property Testing Vergleich:
Example-Based | Property-Based |
---|---|
10 handpicked cases | 1000+ generated cases |
„Works for my examples“ | „Works mathematically“ |
Misses edge cases | Finds edge cases automatically |
Tests behavior | Tests invariants |
sort([1,2,3]) = [1,2,3] | ∀ lists: sort(list) is ordered |
Wie Hari Seldon’s Psychohistory: Statt einzelne Events vorherzusagen, beweisen wir mathematische Gesetze über alle möglichen Zustände! 📊
🧬 TDD: Der ultimative Entwicklungs-Kulturschock
🚀 Kombination: Property-Based Testing + TDD
Der ultimative Workflow für Nova’s TaskApp:
// 1. TDD: Red Phase - Property als Hypothese @Property @DisplayName("🧬🔬 Task assignment should preserve task validity") void taskAssignmentPreservesValidity(@ForAll("validTasks") Task task, @ForAll("validUsers") User user) { assumeThat(task.isValid()).isTrue(); // Hypothese: Task Assignment ändert nichts an Task-Gültigkeit task.assignTo(user); assertThat(task.isValid()).isTrue(); // Wird erstmal FAIL! } // 2. TDD: Green Phase - Minimal Implementation public class Task { private User assignee; public void assignTo(User user) { if (user != null && user.isActive()) { this.assignee = user; } // Property-Test zwingt zu: Validation bleibt erhalten! } } // 3. TDD: Refactor Phase - Elegant machen public class Task { public void assignTo(User user) { validateAssignment(user); this.assignee = user; // Tests garantieren: Refactoring bricht nichts! } private void validateAssignment(User user) { if (user == null) { throw new IllegalArgumentException("User cannot be null"); } if (!user.isActive()) { throw new IllegalStateException("Cannot assign to inactive user"); } } }
Das Ergebnis: Wissenschaftlich bewiesener Code! 🧬🔬
💡 Deine Action Items für diese Woche
🎯 Level 1: Property-Based Testing ausprobieren (heute)
- jqwik zu Maven/Gradle hinzufügen
- jqwik Documentation
- Eine @Property für bestehenden Code schreiben
- Lass jqwik laufen: Welche Edge Cases findet es?
🎯 Level 2: TDD Basics – aber REALISTISCH! (diese Woche)
- Schreibe einen Test BEVOR du Code schreibst (nur einmal!)
- Erwarte Frustration – das ist normal! TDD braucht 2-3 Wochen Eingewöhnung
- Starte winzig klein: Nur simple Getter/Setter-Tests am Anfang
🎯 Level 3: Team-Champion werden (nächster Monat)
Challenge: „TDD Culture Change Agent“
- Führe TDD für EIN kleines Feature komplett durch
- Mache Pair-Programming mit Kollegen
- Dokumentiere messbare Vorteile (weniger Bugs, schnelleres Debugging)
- Teile „Vorher/Nachher“ Erfahrungen mit Community
🎉 Fazit: Tests als wissenschaftliche Methode
Was wir heute gelernt haben:
🧬 TDD = Evolutionsbiologie für Code
- Tests definieren Überlebensbedingungen
- Code adaptiert sich automatisch
- Requirements ändern sich → Code evolviert mit
🔬 Property-Based Testing = Mathematische Beweise
- Statt Beispiele → teste Gesetzmäßigkeiten
- jqwik findet Edge Cases automatisch
- Code wird mathematisch korrekt
🚀 Kombination = Wissenschaftliche Software-Entwicklung
- TDD für Evolution
- Properties für Invarianten
- Ergebnis: Bewiesener Code!
Für Nova und alle Lernenden:
Fangt klein an! Ein TDD-Zyklus pro Tag. Eine Property pro Woche. Evolution braucht Zeit, aber die Ergebnisse sind spektakulär!
Wie Miller’s Detective-Work: Follow the evidence, work the problem! 🕵️
📝 Kurze Zusammenfassung
🧬 TDD = Evolution:
- Red-Green-Refactor = Survival of the Fittest
- Tests = Umweltdruck → Code adaptiert sich
- Requirements ändern sich → Code evolviert mit
🔬 Property-Based Testing:
- jqwik generiert 1000+ Testfälle automatisch
- Mathematische Properties statt Einzelbeispiele
- Findet Edge Cases, die Menschen übersehen
🎯 Nächste Woche: Mutation Testing – testen wir unsere Tests!
❓ FAQ – TDD & Property-Based Testing
Frage 1: TDD fühlt sich langsam an und das Team widersteht – normal?
Antwort: Absolut normal! 80% der Teams durchlaufen 2-3 Monate Widerstand. Wie Rauchen aufhören – kurzfristig schmerzhaft, langfristig lebensrettend. Franz-Martin’s Tipp: Start mit Bug-Fix-TDD, nicht neuen Features.
Frage 2: Wie überzeuge ich Senior-Entwickler von TDD?
Antwort: Nicht durch Überreden, sondern durch Zeigen! Nimm das nächste komplexe Feature, mache es TDD, dokumentiere weniger Bugs und schnelleres Debugging. Pair-Programming ist effektiver als Workshops.
Frage 3: Manager sagt „TDD dauert zu lang“ – Antworten?
Antwort: Zahlen sprechen: „TDD = 15% mehr Zeit entwickeln, 40% weniger Zeit debuggen.“ Rechne Production-Bugs und Hotfixes gegen TDD-Zeit. Ein Production-Bug kostet oft mehr als 1 Woche TDD.
Frage 4: Legacy Code ist nicht testbar – TDD unmöglich?
Antwort: Charakterisierungstests first! Beschreibe IST-Zustand mit Tests, dann schrittweise refactoren. Michael Feathers „Working Effectively with Legacy Code“ ist Pflichtlektüre.
Frage 5: Team macht „Test-Theater“ (Tests nach Code) – wie stoppen?
Antwort: Code-Reviews fokussieren auf Commit-History. TDD-Commits sehen anders aus: Test-Commit → Code-Commit → Refactor-Commit. Erklärt Red-Green-Refactor-Zyklen in Reviews.
Dr. Cassian Holt verbindet wissenschaftliche Methodik mit praktischer Software-Entwicklung. Seine Testing-Serie zeigt, wie Biologie, Mathematik und Physik besseren Code ermöglichen.
Tags: #Testing #TDD #PropertyBasedTesting #jqwik #Evolution #Mathematics #QualityAssurance #SoftwareEngineering
0 Kommentare