Published Sep 16, 2025 ⦁ Updated Oct 11, 2026 ⦁ 15 min read
Automated Test Case Generation: Techniques, Tools, Examples

Automated Test Case Generation: Techniques, Tools, Examples

Automated test case generation means a tool, not a person, produces the test inputs (and sometimes the assertions). The approaches that work in practice today are property-based testing (Hypothesis, fast-check), fuzzing, combinatorial generation (PICT), search-based unit test generators (EvoSuite, Randoop, Pynguin), model-based testing, and LLM-generated tests (GitHub Copilot, Playwright's test agents). Each one finds different bugs, and none replaces knowing what correct behavior looks like.

This guide shows each technique with a small runnable example, where it fits, and its limits, as of October 2026.

Why generate test cases automatically?

Hand-written tests check the cases you thought of. Generated tests explore the ones you didn't: empty strings, huge numbers, Unicode, odd call orders, rare config combinations. Automating test case creation is also how teams add coverage to legacy code that has none.

The catch is the oracle problem: a generator can produce millions of inputs, but something still has to decide whether each output is correct. How each technique answers that question is the main difference between them.

Technique Who decides "correct"? Best for Example tools
Property-based testing Properties you write Pure logic, parsers, serializers Hypothesis, fast-check, jqwik
Fuzzing Crashes, sanitizers, assertions Parsers, native code, security libFuzzer, Go fuzzing, Atheris, Jazzer
Combinatorial (pairwise) You, per generated case Config and input combinations PICT
Search-based unit generation Current behavior (regression) Legacy Java/Python coverage EvoSuite, Randoop, Pynguin
Model-based testing A model of states and transitions Stateful systems, APIs, UIs Hypothesis stateful, fast-check commands
LLM-generated tests The model's guess, then you Boilerplate unit and E2E tests Copilot /tests, Playwright agents

Property-based testing

Instead of writing examples, you write a property that should hold for all inputs, and the library generates inputs and shrinks any failure to the smallest one.

Python with Hypothesis (pip install hypothesis pytest):

# test_slug.py
from hypothesis import given, strategies as st

def slugify(text: str) -> str:
    return "-".join(text.lower().split())

@given(st.text())
def test_slug_has_no_spaces(text):
    assert " " not in slugify(text)

@given(st.text())
def test_slugify_is_idempotent(text):
    assert slugify(slugify(text)) == slugify(text)

Run pytest. Hypothesis tries 100 inputs per test by default, including empty strings, odd whitespace and Unicode. Now try assert slugify(text) != "" and it immediately reports a minimal failing input (text=''). That shrinking step is why the failure reports are readable.

JavaScript/TypeScript with fast-check (npm install --save-dev fast-check), in any test runner:

import fc from 'fast-check';
import { test } from 'vitest';

const sortNumbers = (xs) => [...xs].sort((a, b) => a - b);

test('sorting keeps length and order', () => {
  fc.assert(
    fc.property(fc.array(fc.integer()), (xs) => {
      const out = sortNumbers(xs);
      return out.length === xs.length && out.every((v, i) => i === 0 || out[i - 1] <= v);
    }),
  );
});

Good properties to look for: round trips (decode(encode(x)) == x), idempotence, invariants (length, totals), and comparing against a simple slow reference implementation.

Limits: you need to think in properties, and generators for complex domain objects take work.

Fuzzing

Fuzzers mutate inputs at high speed, guided by code coverage, and report inputs that crash, hang, or trip a sanitizer. Go has it built in since 1.18:

// reverse_test.go
package reverse

import (
	"testing"
	"unicode/utf8"
)

// Reverse has a bug: it reverses bytes, not characters.
func Reverse(s string) string {
	b := []byte(s)
	for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
		b[i], b[j] = b[j], b[i]
	}
	return string(b)
}

func FuzzReverse(f *testing.F) {
	f.Add("hello") // seed corpus
	f.Fuzz(func(t *testing.T, s string) {
		rev := Reverse(s)
		if Reverse(rev) != s {
			t.Errorf("double reverse of %q gave %q", s, Reverse(rev))
		}
		if utf8.ValidString(s) && !utf8.ValidString(rev) {
			t.Errorf("Reverse produced invalid UTF-8 from %q: %q", s, rev)
		}
	})
}

Run go test -fuzz=FuzzReverse -fuzztime=30s. Within seconds the fuzzer finds a multi-byte character (any non-ASCII letter) that breaks the UTF-8 check, the same bug as in the official Go fuzzing tutorial. Failing inputs are saved under testdata/fuzz/ and replayed as regular tests on every go test. For C/C++, Python, Java and continuous fuzzing with OSS-Fuzz, see open source fuzzing tools for beginners.

Limits: great at crashes and memory bugs, weak at wrong-but-not-crashing results unless you add assertions.

Combinatorial (pairwise) test generation

When a feature has many parameters, testing every combination explodes. Pairwise generation covers every pair of values with far fewer cases, and most configuration bugs involve one or two parameters. Microsoft's open-source PICT takes a model file:

Browser: Chrome, Firefox, Safari
OS:      Windows, macOS, Linux
Locale:  en, de, ja
Plan:    Free, Pro

pict model.txt prints a table of test cases (a handful of rows instead of the 54 full combinations). Constraints such as IF [Browser] = "Safari" THEN [OS] = "macOS"; remove impossible rows. You still write the expected result for each row, so this generates test cases, not test code.

Search-based unit test generation (Java, Python)

These tools analyze your classes, generate sequences of constructor and method calls, and keep the ones that increase coverage. The assertions record what the code currently does, so the output is a regression suite: it catches future changes, not existing bugs.

  • Randoop (Java): feedback-directed random generation of JUnit tests. It produces regression tests and "error-revealing" tests when it finds contract violations such as an exception from equals. Latest release 4.3.4 (June 2025).
  • EvoSuite (Java): evolutionary search for high-coverage JUnit suites. Widely used in research, but the last release (1.2.0) is from September 2021, so check it supports your JDK before relying on it.
  • Pynguin (Python): generates pytest tests for a module. Python 3.10 is the supported version (3.11 to 3.14 experimental), and it executes the module under test, so run it in a container:
pynguin --project-path ./src --output-path ./generated-tests --module-name mypkg.pricing

Commercial options exist too, such as Diffblue Cover for Java.

Limits: generated tests are often hard to read, and if the code has a bug, the generated assertion locks the bug in. Review them like any other code.

Model-based testing

You describe the system as states and allowed actions, and the tool generates random action sequences, checking the real system against your model after each step. This finds bugs that only appear after a particular order of calls. Hypothesis has stateful testing (RuleBasedStateMachine) and fast-check has model-based commands:

from hypothesis.stateful import RuleBasedStateMachine, rule, invariant
from hypothesis import strategies as st

class CartMachine(RuleBasedStateMachine):
    def __init__(self):
        super().__init__()
        self.cart = {}          # system under test (replace with your real Cart)
        self.model_count = 0    # the simple model

    @rule(sku=st.sampled_from(["a", "b"]), qty=st.integers(1, 5))
    def add(self, sku, qty):
        self.cart[sku] = self.cart.get(sku, 0) + qty
        self.model_count += qty

    @rule(sku=st.sampled_from(["a", "b"]))
    def remove(self, sku):
        self.model_count -= self.cart.pop(sku, 0)

    @invariant()
    def counts_match(self):
        assert sum(self.cart.values()) == self.model_count

TestCart = CartMachine.TestCase

Limits: the model is real work to build and to keep in sync with the product.

Test case generation using generative AI

LLMs read your code and write test code in your framework's style. They are fast at boilerplate and at listing edge cases, and they are the first approach that produces readable tests with meaningful names.

  • GitHub Copilot: open the file and run /tests in Copilot Chat; GitHub's own guide warns the result may not cover every scenario. Keeping an existing test file open in another tab helps it match your conventions.
  • Playwright test agents: npx playwright init-agents --loop=vscode (or claude, codex, opencode) adds three agents: a planner that explores the app and writes a Markdown test plan, a generator that turns the plan into Playwright tests, and a healer that runs the suite and repairs failures (Playwright docs).
  • Recorders: npx playwright codegen https://example.com records your clicks into a test. It isn't AI, but it is often the fastest way to a first E2E test.
  • Coding agents (Claude Code, Codex, Cursor) can write tests, run them, and iterate until they pass. Qodo's open-source Qodo-Cover tool, which did this for coverage, has been unmaintained since June 2025; Qodo now focuses on AI code review.

A loop that works: ask the model for a list of test cases first (inputs and expected results), fix that list yourself, then let it write the code, and run the suite and a coverage report before merging.

Limits: models invent APIs, write tests that assert the current (possibly buggy) behavior, and sometimes "fix" a failing test by weakening the assertion. Never merge generated tests you haven't read. Cost per call is in our AI model pricing comparison, and free options are listed under free GitHub Copilot alternatives.

Which technique should you use?

  1. Pure functions, parsers, data transforms: property-based tests.
  2. Code that parses untrusted input, or native code: fuzzing in CI.
  3. Many configuration options: pairwise generation for the test matrix.
  4. Untested legacy code you need to refactor: search-based regression tests or an LLM to build a safety net, then delete what you don't need.
  5. Stateful APIs or UIs with ordering bugs: model-based tests.
  6. Day-to-day unit and E2E tests: LLM drafts, reviewed by a human.

More tools in each category are in DevHunt's testing tools directory.

FAQ

What is automated test case generation?

It is the use of tools to produce test inputs, test sequences or complete test code automatically, instead of writing each case by hand. Common methods are property-based testing, fuzzing, pairwise generation, search-based generation and LLM-based generation.

Can AI generate test cases from requirements?

Yes, LLMs can turn a user story or acceptance criteria into a list of test cases or Gherkin scenarios. Treat the output as a draft: the model doesn't know your business rules, so a person has to confirm each expected result. See our Gherkin syntax guide for the format.

Are there free tools for automatic test generation?

Hypothesis, fast-check, Go fuzzing, libFuzzer, PICT, Randoop, EvoSuite, Pynguin and Playwright (including codegen and its test agents) are all free and open source. Copilot has a free tier with limited monthly usage.

Do generated tests replace hand-written tests?

No. Generated tests are good at breadth (many inputs, many combinations). Hand-written tests document intent and the cases the business cares about. Most teams use both.

What is the difference between test case generation and test automation?

Test automation runs tests without a person (for example, Playwright in CI). Test case generation creates the tests or their inputs. You can automate hand-written tests, and you can generate tests that you then run automatically.