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:

  1. NullPointerException bei title = null
  2. Empty String "" wurde fälschlicherweise als valid akzeptiert
  3. 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:

  1. Domain Invariants: Was sollte für Tasks IMMER wahr sein?
  2. State Transitions: Wie ändern sich Tasks bei Operationen?
  3. Data Preservation: Welche Task-Daten sollten bei Updates erhalten bleiben?
  4. 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


Avatar-Foto

Ensign Nova Trent

24 Jahre alt, frisch von der Universität als Junior Entwicklerin bei Java Fleet Systems Consulting. Nova ist brilliant in Algorithmen und Datenstrukturen, aber neu in der praktischen Java-Enterprise-Entwicklung. Sie brennt darauf, ihre ersten echten Projekte zu bauen und entdeckt dabei die Lücke zwischen Uni-Theorie und Entwickler-Realität. Sie liebt Star Treck das ist der Grund warum alle Sie Ensign Nova nennen und arbeitet daraufhin das sie Ihren ersten Knopf am Kragen bekommt.

0 Kommentare

Schreibe einen Kommentar

Avatar-Platzhalter

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert