agentleFS
igmarinigmarin/elixir-phoenix-skills/skills/property-based-testing/SKILL.md

property-based-testing

Skill4 starsChanged 4 months agoMITView on GitHub

Use when writing tests that need to cover many input combinations or complex edge cases. Generates custom StreamData generators, writes ExUnitProperties tests, configures shrinking strategies, and creates property-based test patterns for data transformations, algorithms, and state machines. Trigger words: property-based testing, StreamData, ExUnitProperties, generators, fuzzing, shrinking.

18 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use observability, simplify and 3 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • OpenAISkill

    observability

    Vercel Observability expert guidance — Drains (logs, traces, speed insights, web analytics), Web Analytics, Speed Insights, runtime logs, custom events, OpenTelemetry integration, and monitoring dashboards. Use when instrumenting, debugging, or optimizing application performance and user experience on Vercel.

    Also in 3 projects that use this

  • Hugging FaceSkill

    simplify

    Refactor code after tests pass. The "Refactor" phase of Red-Green-Refactor.

    Also in 3 projects that use this

  • Microsoft AzureSkill

    git-workflow

    Squad branching model: dev-first workflow with insiders preview channel

    Also in 3 projects that use this

  • SentrySkill

    security-review

    Finds exploitable application security vulnerabilities in code changes. Use for Warden security scans, appsec review, OWASP-style checks, authentication or authorization bugs, injection, XSS, SSRF, path traversal, secrets, unsafe crypto, webhook verification, open redirects, or sensitive data exposure.

    Also in 3 projects that use this

  • NVIDIASkill

    research

    Before designing a feature, research how comparable libraries solve the problem, check best practices, known pitfalls, and upstream changes. Use when starting a new feature, investigating an unfamiliar area, or the user asks to research something.

    Also in 3 projects that use this

The file

Files
SKILL.md

Property-Based Testing

RULES — Follow these with no exceptions

1. Define properties, not examples — state what should always be true, not specific inputs/outputs 2. Test invariants, not specific values — "output length equals input length" not "output is [1, 2, 3]" 3. Leverage shrinking — let StreamData find minimal failing cases; add constraints like min_length: 1 or integer(1..100) when shrunk cases expose invalid generator inputs

Setup

# mix.exs
defp deps do
  [
    {:stream_data, "~> 1.0", only: :test}
  ]
end

Basic Property Test

defmodule MyApp.StringUtilsTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  property "reversing a string twice returns the original" do
    check all string <- string(:ascii) do
      assert string |> String.reverse() |> String.reverse() == string
    end
  end

  property "length of reversed string equals original length" do
    check all string <- string(:ascii) do
      assert String.length(String.reverse(string)) == String.length(string)
    end
  end
end

Generators

Less Obvious Built-ins

# Combine multiple generators with one_of
one_of([constant(:a), constant(:b), constant(:c)])
one_of([integer(), string(:ascii), boolean()])

# Compose generators with gen all
gen all x <- integer(0..100),
        y <- integer(0..100),
        x != y do
  {x, y}
end

# Non-empty collections
list_of(integer(), min_length: 1)
map_of(string(:alphanumeric), integer())

Custom Generators

# Generate a valid email
def email_generator do
  gen all name <- string(:alphanumeric, min_length: 3),
          domain <- string(:alphanumeric, min_length: 3) do
    "#{name}@#{domain}.com"
  end
end

# Generate a user struct
def user_generator do
  gen all email <- email_generator(),
          age <- integer(18..120) do
    %User{email: email, age: age}
  end
end

# Use in tests
property "users have valid emails" do
  check all user <- user_generator() do
    assert user.email =~ ~r/@.*\.com$/
    assert user.age >= 18 and user.age <= 120
  end
end

Testing Invariants

Key invariants to test for collections: ordering, element preservation, and idempotence.

defmodule MyApp.SortingTest do
  use ExUnit.Case, async: true
  use ExUnitProperties

  # Invariant: sorted list is ordered
  property "sorted list is ordered" do
    check all list <- list_of(integer()) do
      sorted = Enum.sort(list)

      sorted
      |> Enum.chunk_every(2, 1, :discard)
      |> Enum.each(fn [a, b] -> assert a <= b end)
    end
  end

  # Invariant: sorting preserves all elements
  property "sorted list contains same elements" do
    check all list <- list_of(integer()) do
      assert Enum.sort(list) |> Enum.frequencies() == Enum.frequencies(list)
    end
  end
end

Shrinking

StreamData automatically shrinks failing inputs to the smallest example that still fails. The property below intentionally fails to illustrate shrinking output:

property "no list contains its own length as element" do
  check all list <- list_of(integer()) do
    refute list |> Enum.member?(length(list))
  end
end

# StreamData will find and report:
# Failed with generated values: [0]
# (shrunk from something like [1, 5, 0, 3, 2])

Workflow: Write → Run → Interpret → Refine

  1. Write property — define an invariant using check all with appropriate generators
  2. Run test — mix test test/my_test.exs
  3. Interpret shrinking output — on failure, StreamData reports both the original failing input and the shrunk minimal example:
    ** (ExUnit.AssertionError)
    Failed with generated values (after 3 successful runs):
    
        * Clause:    list <- list_of(integer())
          Generated: [0]
          (shrunk from [42, -7, 0, 15])
  4. Refine generator or property — if the shrunk case reveals a generator producing invalid inputs, add constraints (e.g. min_length: 1, integer(1..100)); if it reveals a real bug, fix the implementation
  5. Re-run — confirm the fix holds across new generated cases

Common Pitfalls

❌ Don't✅ Do
Assert exact outputs (== [1, 2, 3])Assert invariants that always hold (ordering, length, membership)
Generate unconstrained inputs that break the code under testConstrain generators (min_length: 1, integer(1..100)) to valid domains
Ignore the shrunk minimal case in failure outputRead the shrunk values first — they point straight at the bug
Use check all without use ExUnitPropertiesAdd use ExUnitProperties alongside use ExUnit.Case
Write one giant property covering many behavioursSplit into focused properties, one invariant each
Leave generators unbounded, causing slow/huge casesBound ranges and collection sizes to keep runs fast

Integration

PredecessorThis SkillSuccessor
testing-essentialsproperty-based-testingNone (standalone)

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.