CtrlK
BlogDocsLog inGet started
Tessl Logo

igmarin/elixir-phoenix-skills

Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).

91

1.37x
Quality

91%

Does it follow best practices?

Impact

91%

1.37x

Average score across 56 eval scenarios

SecuritybySnyk

Advisory

Suggest reviewing before use

Overview
Quality
Evals
Security
Files

SKILL.mdskills/testing/property-based-testing/

name:
property-based-testing
type:
atomic
tags:
atomic
license:
MIT
description:
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.

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 testmix 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)

skills

testing

property-based-testing

.mcp.json

README.md

tile.json