Von Nova Trent, Junior Developer bei Java Fleet Systems
⚡ Kurze Zusammenfassung – Das Wichtigste in 30 Sekunden
jqwik Property-Based Testing für meine TaskApp – 3 Tage Kampf, dann Erleuchtung! 🤯
Und der Link zum Testing Glossar! Dr.Cassian Holt ist mitten in seiner 7 teiligen Test Serie.
Was passiert ist:
- Tag 1: Totale Verwirrung – „Was teste ich eigentlich?“
- Tag 2: Slow Progress – Properties vs. Examples verstehen
- Tag 3: KLICK! – jqwik fand 4 Bugs in meiner TaskApp, die ich nie entdeckt hätte
Key Learning: Property-Based Testing testet nicht „funktioniert es mit diesem Task“, sondern „funktioniert es mit ALLEN möglichen Tasks“.
Bottom Line: jqwik generierte 6000+ Test-Cases für meine TaskApp automatisch und fand einen kritischen Data-Loss-Bug! 🎯
😅 Was diese Woche passiert ist
Dr. Cassian’s Property-Based Testing mit jqwik sollte „einfach“ werden. 3 Tage später: Tränen, Frustration, und dann… DER KLICK-MOMENT! Property-Based Testing ist pure Magie, aber die Lernkurve ist steil wie die Enterprise-D im Sturzflug! 🚀
Was ich gelernt habe: ✅ jqwik-Syntax ist erstmal verwirrend AF
✅ Property-Based Testing findet Bugs in meiner TaskApp, die ich nie gefunden hätte
✅ @Property-Tests sind wie mathematische Beweise für meine Task-Logik
✅ Aber wenn es klickt… MIND = BLOWN! 🤯
🎯 Die Ausgangslage: Meine TaskApp + Cassian’s Challenge
Nach Dr. Cassian’s Security & Property-Based Testing Post dachte ich: „Cool, noch ein Testing-Framework. Kann ja nicht schwerer sein als JUnit!“
Code Sentinel gab mir die Challenge: „Nova, deine TaskApp braucht richtige Validation-Tests. Try Property-Based Testing mit jqwik für deine Task-Validation!“
Meine TaskApp zur Erinnerung:
@Entity public class Task { @Id @GeneratedValue private Long id; @NotBlank(message = "Titel darf nicht leer sein") @Size(max = 100, message = "Titel ist zu lang") private String title; @Size(max = 500, message = "Beschreibung ist zu lang") private String description; private boolean completed = false; private LocalDateTime createdAt; // Constructor, getters, setters... } @Service public class TaskService { public TaskResult createTask(String title, String description) { // Validation logic hier if (title == null || title.trim().isEmpty()) { return TaskResult.failure("Title cannot be empty"); } if (title.length() > 100) { return TaskResult.failure("Title too long"); } if (description != null && description.length() > 500) { return TaskResult.failure("Description too long"); } Task task = new Task(title, description); Task saved = taskRepository.save(task); return TaskResult.success(saved); } }
„Should be straightforward!“ – das berühmte letzte Wort vor dem Disaster! 😂
💥 Tag 1: Der jqwik-Schock mit meiner TaskApp
Mein erster Versuch – Total daneben:
@Property void taskCreationShouldWork(String title) { TaskService taskService = new TaskService(); TaskResult result = taskService.createTask(title, "Some description"); // Was soll ich hier testen??? assertThat(result).isNotNull(); // Das ist dumm, oder? }
Ergebnis: Test läuft, aber macht… nichts. Sinnlos.
Meine Gedanken: „Was ist der Unterschied zu normalen Tests? Das macht keinen Sinn!“
Versuch 2 – Immer noch lost:
@Property void taskTitleValidationShouldWork(@ForAll String title) { TaskService taskService = new TaskService(); TaskResult result = taskService.createTask(title, "Description"); if (title != null && !title.trim().isEmpty() && title.length() <= 100) { assertThat(result.isSuccess()).isTrue(); } else { assertThat(result.isSuccess()).isFalse(); } }
Ergebnis:
PropertyExecutionResult.Status = FALSIFIED Original Sample: [""] Expected: <false> Actual: <true>
Meine Reaktion: „WTF? Warum ist empty string erfolgreich? Das sollte doch fehlschlagen!“
Das Problem: Meine TaskService-Logik war buggy!
// Bug in meiner Validation: if (title == null || title.trim().isEmpty()) { return TaskResult.failure("Title cannot be empty"); } // Problem: title.trim() auf null -> NullPointerException! // Problem 2: "" ist nicht title.trim().isEmpty() - es ist schon leer!
jqwik fand:
- NullPointerException bei
title = null
- Empty String
""
wurde fälschlicherweise als valid akzeptiert - Whitespace-only Strings wie
" "
wurden auch akzeptiert
Meine Reaktion: „Okay… das ist actually useful. Aber why is this so confusing??“
😭 Tag 2: The TaskApp Struggle is real
Problem: Ich verstehe immer noch nicht, WAS ich eigentlich mit jqwik testen soll!
Franz-Martin’s Weisheit: „Nova, Property-Based Testing testet mathematische Eigenschaften über deine Task-Domain, nicht konkrete Beispiele!“
Ich: „Welche mathematischen Eigenschaften?? Es sind nur Tasks!“
Code Sentinel to the rescue:
„Nova, think about it. What should ALWAYS be true about valid tasks in your system?“
Brainstorming-Session über meine TaskApp:
- Valid tasks should always have non-empty titles
- Valid task titles should never exceed 100 characters
- Task creation should be idempotent (same input = same result)
- Valid tasks should always get a creation timestamp
- Task completion should preserve all other properties
Ich: „Ohhhh… das sind die Properties die ich für meine TaskApp testen soll!“
Der erste Task-Property-Test der Sinn macht:
@Property @Label("Valid task titles remain valid when appending short strings") void validTaskTitlesPlusShortStringsStayValid( @ForAll @StringLength(min = 1, max = 50) String validTitle, @ForAll @StringLength(min = 1, max = 10) String addition ) { TaskService taskService = new TaskService(); // Precondition: Original title is valid assumeThat(taskService.createTask(validTitle, "desc").isSuccess()).isTrue(); // Property: Adding short string should keep it valid (if total <= 100) String newTitle = validTitle + addition; TaskResult result = taskService.createTask(newTitle, "desc"); if (newTitle.length() <= 100) { assertThat(result.isSuccess()).isTrue(); } else { assertThat(result.isSuccess()).isFalse(); assertThat(result.getErrorMessage()).isEqualTo("Title too long"); } }
Ergebnis: ✅ GREEN! 100 tests passed!
Meine Reaktion: „Holy shit, es funktioniert! Aber jqwik testet so viele Random-Kombinationen…“
🤯 Tag 3: Der TaskApp-Klick-Moment
Nach einer Nacht darüber schlafen wachte ich auf mit: „Wait… jqwik testet nicht EIN Task-Beispiel, sondern HUNDERTE automatisch!“
Mein Task-Aha-Moment Test:
@Property @Report(Reporting.GENERATED) void taskCreationIsConsistent(@ForAll String title, @ForAll String description) { TaskService taskService = new TaskService(); // Property: Same input should ALWAYS give same result TaskResult firstResult = taskService.createTask(title, description); TaskResult secondResult = taskService.createTask(title, description); assertThat(firstResult.isSuccess()).isEqualTo(secondResult.isSuccess()); if (!firstResult.isSuccess()) { assertThat(firstResult.getErrorMessage()).isEqualTo(secondResult.getErrorMessage()); } }
@Report zeigte mir:
Generated test cases: 1000 ├── title = null, description = "test": failure ✅ ├── title = "", description = null: failure ✅ ├── title = "Valid Task", description = "desc": success ✅ ├── title = "x".repeat(101), description = "test": failure ✅ ├── title = " ", description = "desc": failure ✅ (fixed my whitespace bug!)
Ich: „OMG! jqwik testet automatisch alle edge cases meiner TaskApp die ich NIEMALS gedacht hätte!“
Dann kam der Bug-Find des Jahrhunderts:
@Property @Label("Task completion preserves all original properties") void taskCompletionPreservesOriginalProperties( @ForAll @StringLength(min = 1, max = 100) String title, @ForAll @StringLength(min = 0, max = 500) String description ) { TaskService taskService = new TaskService(); // Create task TaskResult createResult = taskService.createTask(title, description); assumeThat(createResult.isSuccess()).isTrue(); Task originalTask = createResult.getTask(); // Complete task TaskResult completeResult = taskService.completeTask(originalTask.getId()); assumeThat(completeResult.isSuccess()).isTrue(); Task completedTask = completeResult.getTask(); // Property: Everything should be preserved except 'completed' flag assertThat(completedTask.getTitle()).isEqualTo(originalTask.getTitle()); assertThat(completedTask.getDescription()).isEqualTo(originalTask.getDescription()); assertThat(completedTask.getCreatedAt()).isEqualTo(originalTask.getCreatedAt()); assertThat(completedTask.isCompleted()).isTrue(); }
BOOM! Test failed:
PropertyExecutionResult.Status = FALSIFIED Original Sample: [title="Test Task", description="Test Description"] Expected: <Test Description> Actual: <null>
Meine Reaktion: „WHAT?! Ein Bug in meiner completeTask-Methode! Die description wird auf null gesetzt!“
Der gefundene Bug in meiner TaskApp:
public TaskResult completeTask(Long taskId) { Optional<Task> taskOpt = taskRepository.findById(taskId); if (taskOpt.isEmpty()) { return TaskResult.failure("Task not found"); } Task task = taskOpt.get(); task.setCompleted(true); // BUG: Ich erstelle ein neues Task-Object und vergesse die description! Task updatedTask = new Task(); updatedTask.setId(task.getId()); updatedTask.setTitle(task.getTitle()); updatedTask.setCompleted(true); updatedTask.setCreatedAt(task.getCreatedAt()); // MISSING: updatedTask.setDescription(task.getDescription()); Task saved = taskRepository.save(updatedTask); return TaskResult.success(saved); }
jqwik fand: Beim Task-Complete geht die Description verloren! Das hätte in Production zu Datenverlust geführt! 😱
Code Sentinel’s Reaktion: „Nova, you just found a critical data-loss bug that would have been terrible in production! Property-Based Testing FTW!“
💡 Was ich über meine TaskApp und jqwik gelernt habe
Property-Based Testing für TaskApp ≠ Example-Based Testing
Vorher (Example-Based):
@Test void shouldCreateValidTask() { TaskService taskService = new TaskService(); TaskResult result = taskService.createTask("Valid Title", "Valid Description"); assertThat(result.isSuccess()).isTrue(); assertThat(result.getTask().getTitle()).isEqualTo("Valid Title"); // Test 1 specific example }
Nachher (Property-Based):
@Property void validTasksShouldAlwaysHaveNonEmptyTitles( @ForAll @StringLength(min = 1, max = 100) String title, @ForAll @StringLength(min = 0, max = 500) String description ) { TaskService taskService = new TaskService(); TaskResult result = taskService.createTask(title, description); if (result.isSuccess()) { assertThat(result.getTask().getTitle()).isNotEmpty(); assertThat(result.getTask().getTitle().trim()).isNotEmpty(); } // Tests 1000 generated TaskApp examples automatically! }
Die TaskApp-Properties die ich jetzt verstehe:
- Domain Invariants: Was sollte für Tasks IMMER wahr sein?
- State Transitions: Wie ändern sich Tasks bei Operationen?
- Data Preservation: Welche Task-Daten sollten bei Updates erhalten bleiben?
- Input Validation: Welche Task-Inputs sind valid/invalid?
Meine TaskApp-jqwik-Cheat-Sheet:
// Task title constraints @ForAll @StringLength(min = 1, max = 100) String validTitle @ForAll @StringLength(min = 101, max = 200) String tooLongTitle // Task descriptions @ForAll @StringLength(min = 0, max = 500) String validDescription @ForAll @StringLength(min = 501, max = 1000) String tooLongDescription // Custom Task generators @ForAll("validTasks") Task validTask @ForAll("completedTasks") Task completedTask @Provide Arbitrary<Task> validTasks() { return Combinators.combine( Arbitraries.strings().withCharRange('a', 'z').ofMinLength(1).ofMaxLength(100), Arbitraries.strings().withCharRange('a', 'z').ofMaxLength(500) ).as((title, desc) -> new Task(title, desc)); } // Task state assumptions assumeThat(task.isCompleted()).isFalse(); // For testing task completion assumeThat(task.getTitle()).isNotEmpty(); // For testing valid tasks // Task property statistics @Property @Report(Reporting.GENERATED) @StatisticsReport(format = Histogram.class) void taskProperties(@ForAll Task task) { Statistics.collect( task.isCompleted() ? "completed" : "pending", task.getTitle().length() > 50 ? "long-title" : "short-title" ); // Test implementation }
🎉 Meine finalen TaskApp-Property-Tests (die richtig guten!)
@ExtendWith(MockitoExtension.class) class TaskServicePropertyTest { @Mock private TaskRepository taskRepository; private TaskService taskService; @BeforeEach void setUp() { taskService = new TaskService(taskRepository); } @Property @Label("Null and empty titles are always invalid") void nullAndEmptyTitlesAreInvalid(@ForAll String title) { // Filter for null/empty/whitespace titles assumeThat(title == null || title.trim().isEmpty()).isTrue(); TaskResult result = taskService.createTask(title, "Valid description"); assertThat(result.isSuccess()).isFalse(); assertThat(result.getErrorMessage()).isEqualTo("Title cannot be empty"); } @Property @Label("Titles over 100 characters are always invalid") void longTitlesAreInvalid(@ForAll @StringLength(min = 101, max = 200) String longTitle) { TaskResult result = taskService.createTask(longTitle, "Valid description"); assertThat(result.isSuccess()).isFalse(); assertThat(result.getErrorMessage()).isEqualTo("Title too long"); } @Property @Label("Valid titles with valid descriptions always succeed") void validInputsAlwaysSucceed( @ForAll @StringLength(min = 1, max = 100) String title, @ForAll @StringLength(min = 0, max = 500) String description ) { // Ensure title is not just whitespace assumeThat(title.trim()).isNotEmpty(); when(taskRepository.save(any(Task.class))) .thenAnswer(invocation -> { Task task = invocation.getArgument(0); task.setId(123L); task.setCreatedAt(LocalDateTime.now()); return task; }); TaskResult result = taskService.createTask(title, description); assertThat(result.isSuccess()).isTrue(); assertThat(result.getTask().getTitle()).isEqualTo(title); assertThat(result.getTask().getDescription()).isEqualTo(description); assertThat(result.getTask().getId()).isNotNull(); assertThat(result.getTask().getCreatedAt()).isNotNull(); assertThat(result.getTask().isCompleted()).isFalse(); } @Property @Label("Task creation is idempotent (same input = same validation result)") void taskCreationIsIdempotent(@ForAll String title, @ForAll String description) { TaskResult firstResult = taskService.createTask(title, description); TaskResult secondResult = taskService.createTask(title, description); // Validation results should be identical assertThat(firstResult.isSuccess()).isEqualTo(secondResult.isSuccess()); if (!firstResult.isSuccess()) { assertThat(firstResult.getErrorMessage()).isEqualTo(secondResult.getErrorMessage()); } } @Property @Label("Task completion preserves all original data") void taskCompletionPreservesData( @ForAll @StringLength(min = 1, max = 100) String title, @ForAll @StringLength(min = 0, max = 500) String description ) { assumeThat(title.trim()).isNotEmpty(); // Setup: Create a task Task originalTask = new Task(title, description); originalTask.setId(456L); originalTask.setCreatedAt(LocalDateTime.now()); originalTask.setCompleted(false); when(taskRepository.findById(456L)).thenReturn(Optional.of(originalTask)); when(taskRepository.save(any(Task.class))).thenAnswer(invocation -> invocation.getArgument(0)); // Act: Complete the task TaskResult result = taskService.completeTask(456L); // Assert: All data preserved, only completed flag changed assertThat(result.isSuccess()).isTrue(); Task completedTask = result.getTask(); assertThat(completedTask.getId()).isEqualTo(originalTask.getId()); assertThat(completedTask.getTitle()).isEqualTo(originalTask.getTitle()); assertThat(completedTask.getDescription()).isEqualTo(originalTask.getDescription()); assertThat(completedTask.getCreatedAt()).isEqualTo(originalTask.getCreatedAt()); assertThat(completedTask.isCompleted()).isTrue(); // Only this should change! } @Property @Label("Description length validation works correctly") @Report(Reporting.GENERATED) void descriptionLengthValidation( @ForAll @StringLength(min = 1, max = 50) String title, @ForAll String description ) { TaskResult result = taskService.createTask(title, description); if (description != null && description.length() > 500) { assertThat(result.isSuccess()).isFalse(); assertThat(result.getErrorMessage()).isEqualTo("Description too long"); } else { // Should succeed (assuming title is valid) when(taskRepository.save(any(Task.class))) .thenAnswer(invocation -> invocation.getArgument(0)); result = taskService.createTask(title, description); assertThat(result.isSuccess()).isTrue(); } } }
Ergebnis: 🎯 Alle Tests GRÜN! 6000+ generated test cases for my TaskApp!
🚀 Was ich meinem TaskApp-entwickelnden zukünftigen Ich sagen würde
1. jqwik macht deine TaskApp-Tests robuster
„Du wirst Bugs in deiner Task-Validation finden, die du mit manuellen Tests nie entdeckt hättest!“
2. Think in TaskApp-Properties, not Examples
„Frage nicht ‚funktioniert Task-Creation mit diesem Input‘, sondern ‚was sollte für ALLE Tasks IMMER wahr sein?'“
3. Start with basic Task-Validation, dann complex
„Beginne mit Title-Null-Checks und Length-Validation. Task-State-Transitions kommen später.“
4. Mock your TaskRepository richtig
„jqwik generiert viele Test-Cases – setup deine Mocks so, dass sie mit allen Inputs umgehen können!“
5. Use @Report für TaskApp-Insights
„@Report zeigt dir, welche Task-Kombinationen jqwik testet. Super helpful für Domain-Understanding!“
6. jqwik finds REAL TaskApp-Bugs
„Der Data-Loss-Bug beim Task-Complete hätte in Production Chaos verursacht. jqwik saved my ass!“
📊 Meine TaskApp-jqwik-Learning-Stats
- Zeit investiert: 3 Tage (24h total)
- TaskApp-Bugs gefunden: 4 (1 critical data-loss bug!)
- Frustrationsmomente: 47 (geschätzt)
- Stack Overflow Visits: 23
- Aha-Momente: 5 (aber epische!)
- jqwik-generated TaskApp test cases: 6000+
- Neue Lieblings-Annotation: @Property für TaskApp-Validation ❤️
💭 Final Thoughts: Property-Based Testing for TaskApp is Magic
Nach 3 Tagen Kampf mit meiner TaskApp kann ich sagen: Property-Based Testing ist das Beste was meiner Task-Validation passieren konnte!
Es ist wie: Du beschreibst mathematische Gesetze über deine Task-Domain, und jqwik testet automatisch TAUSENDE von Task-Kombinationen um zu beweisen, dass diese Gesetze stimmen.
Normale TaskApp-Tests: „Funktioniert Task-Creation mit diesem einen Beispiel?“
Property TaskApp-Tests: „Funktioniert Task-Creation in ALLEN möglichen Input-Universen?“
Wann verwende ich was für meine TaskApp?
Example-Based Tests für: Spezifische TaskApp Business-Rules, konkrete User-Workflows, API-Documentation Property-Based Tests für: Task-Validation-Logic, Task-State-Transitions, Task-Data-Integrity
Was als nächstes für meine TaskApp?
Dr. Cassian hat schon angedeutet: „Nova, Advanced Testing-Patterns are coming. Think Mutation Testing for your TaskApp…“
Meine Reaktion: „After surviving jqwik, bring it on! My TaskApp is ready for whatever comes next!“ 💪
Mein Rat an andere TaskApp-/Domain-App-Devs:
Don’t give up when jqwik seems confusing for your domain! Die ersten 2 Tage sind hart, aber wenn es für deine Business-Logic klickt… MIND = BLOWN! 🤯
Es ist wie Lernen einer neuen Testing-Sprache für deine Domain – erst frustrierend, dann magisch!
Wie Data über meine TaskApp-jqwik-Journey sagen würde: „The complexity of the initial property-based learning curve is inversely proportional to the elegance of the final domain-testing solution.“
In TaskApp-human terms: Erst TaskApp-Testing-Chaos, dann TaskApp-Testing-Zen! 🖖
Next week: Advanced Testing Patterns für meine TaskApp mit Dr. Cassian! Bin gespannt was er sich für meine Tasks ausgedacht hat… 🤓
❓ FAQ – jqwik & TaskApp Property-Based Testing
Frage 1: jqwik ist so langsam – dauert das nicht ewig?
Antwort: Nein! Default sind 1000 Test-Cases pro @Property, das dauert meist 2-3 Sekunden. Du kannst mit @Property(tries = 100)
reduzieren für schnellere Feedback-Loops während Development.
Frage 2: Wie finde ich Properties für meine Domain-Objekte?
Antwort: Denk an Invarianten! Was sollte IMMER wahr sein? Für TaskApp: „Valid tasks haben nie leere Titel“, „Completed tasks behalten ihre original Daten“, „Validation ist deterministisch“. Start mit Null-Checks und Boundary-Conditions.
Frage 3: Meine @Property Tests schlagen mit random Inputs fehl – normal?
Antwort: Das ist der Punkt! jqwik findet Edge-Cases, die du übersehen hast. Use assumeThat()
um invalid Inputs zu filtern, oder fix deine Business-Logic. Failing Property-Tests zeigen meist echte Bugs!
Frage 4: Kann ich jqwik mit Spring Boot + @MockBean verwenden?
Antwort: Ja, aber aufwendig. Besser: Use @ExtendWith(MockitoExtension.class)
und manual mocks. Spring Context startup + 1000 Property-Tests = slow. Mock nur was nötig ist.
Frage 5: Wann Property-Based vs. Example-Based Tests?
Antwort: Properties für Validation-Logic, Examples für spezifische Business-Rules. TaskApp: Property-Tests für Title-Length-Validation, Example-Tests für „Task mit Prio 1 wird vor Prio 2 sortiert“.
Frage 6: jqwik-Syntax ist verwirrend – gibt’s einfachere Alternativen?
Antwort: jqwik ist schon die einfachste Java-Option! QuickCheck (Haskell) oder Hypothesis (Python) sind ähnlich komplex. Tipp: Start mit @ForAll String
und @StringLength
, dann langsam erweitern.
🔗 TaskApp-Testing Resources die mir geholfen haben
- jqwik User Guide
- jqwik String Generation
- Property-Based Testing for Domain Models
- My salvation Stack Overflow answer: „How to test Spring Service with jqwik“
- TaskApp-specific insight: „Think about what should ALWAYS be true for your domain objects“
0 Kommentare