Using AI as a Red Team for Property-Based Testing

Why using LLMs for basic unit tests is a missed opportunity, and how to use them to stress-test your system invariants instead.

Every engineer knows the trap of unit testing. You write a complex function or Playwright test, then immediately write three or four assertions that validate the exact happy paths and edge cases you had in mind while writing the code. The problem is cognitive bias: you rarely write tests for the bugs you failed to foresee.

The Flaw in Static Validation

When AI coding assistants entered the developer toolkit, most engineers started using them as high-speed autocomplete - asking them to generate repetitive boilerplate or basic expect() statements. But generating thirty generic assertions that check the same predictable paths doesn’t raise the reliability bar of a codebase. It just inflates your line count.

In my day-to-day workflow, I treat LLMs less like a junior developer writing syntax and more like an automated “red team” partner - specifically designed to help construct property-based tests that break my assumptions before code hits production.

Static Example Assertions vs. Invariants

Standard testing in Playwright checks specific example inputs against expected outputs:

Input X gives output Y.

Property-based testing flips this model. Instead of hardcoding static inputs, you define an invariant - a universal property that must always hold true across hundreds of generated inputs:

For any valid payload or dynamic input sequence P, running the Playwright test loop should maintain state integrity: $F(F(P)) == F(P)$.

The Cognitive Barrier

The challenge with property-based testing isn’t running the test runner; it’s the mental model required to write them. Distilling complex, stateful application logic down to abstract invariants is tough, and it’s easy to miss subtle state combinations when setting up dynamic parameters in Playwright.

How do we move from “generating assertions” to “stress-testing system logic”?

The Adversarial Workflow

Instead of asking an LLM to “write a Playwright test for this component,” I feed it an abstract specification, interface contract, or data model, and ask it to act as an adversary.

This approach targets three specific architectural blind spots:

  • Extracting Universal Invariants: I provide a component interface or API contract and ask the model what properties must logically hold true regardless of the input value, or what layout/state invariants should remain unchanged across variations.
  • Finding Hidden State Transitions: When dealing with multi-step workflows in Playwright (e.g., checkout flows or form wizards), I ask the model to map out invalid state transitions that my standard user flow ignores.
  • Generating Parameterized Matrix Fixtures: Once the invariants are defined, I use the model to generate dynamic data loops and custom parameter matrices for Playwright (test.describe loops). Instead of testing with simple strings, the model generates payloads targeting complex edge cases: zero-width spaces, deeply nested JSON objects, boundary timestamps, or unnormalized Unicode.

Examples:

Catching the Unforeseen Edge Case

Recently, while working on a data-parsing and display component tested with Playwright, my manual test suite passed with 100% assertions green.

  • The Reality: Standard Playwright assertions confirmed that individual transactions rendered correctly based on known static fixtures.
  • The Shift: Rather than shipping it, I handed the data contract to an LLM with a simple prompt: “Design three property-based invariants and a randomized data generator loop to stress-test rounding accumulation errors and rapid UI state re-renders in Playwright.”

It pointed out that while individual transaction calculations were correct, feeding randomized, high-frequency currency streams into the component exposed a subtle precision loss and layout shift over time. It then drafted a Playwright parameterized test loop that generated hundreds of dynamic payload variations to prove the failure mode.

The bug was caught locally in 10 minutes, rather than as an intermittent UI glitch or calculation discrepancy in production three months later.

Elevating the Engineering Loop

Using AI effectively in modern software engineering isn’t about delegating your critical thinking or automating away code writing. It’s about using the tool to scale your own analytical reach.

By shifting the conversation with AI from “write my boilerplate” to “help me stress-test my architectural assumptions,” you build systems that are fundamentally resilient. You stop writing Playwright tests to prove your code works, and start designing systems capable of proving they won’t fail.