Minigun
A QuickCheck-like library for property-based unit-testing of Python programs.
Minigun is inspired by QCheck, which in turn was inspired by QuickCheck. Both are libraries that provide implementations for performing property-based unit-testing; for OCaml and Haskell respectively.
If you would like a bit of motivation as to why you should use a QuickCheck-like system for testing your project, then I would recommend that you watch:
If you wish to learn more about the subject, I can recommend Jan Midtgaard's lecture materials; it is OCaml based but translates easily to other QuickCheck-like libraries for other languages.
Install
Minigun requires Python >=3.12. It is distributed with pip and can be installed with the following command:
pip install minigun-soren-n
Quick Start
Using the CLI (Recommended)
Create a test module in the tests/ directory:
# tests/my_tests.py
from minigun import prop, conj
@prop("reversing a list twice gives the original")
def test_reverse(lst: list[int]) -> bool:
return list(reversed(list(reversed(lst)))) == lst
@prop("list length distributes over concatenation")
def test_length(xs: list[int], ys: list[int]) -> bool:
return len(xs + ys) == len(xs) + len(ys)
spec = conj(test_reverse, test_length)
Run your tests with a time budget:
minigun --time-budget 30
Using as a Library
from minigun import prop, check
@prop("reversing a list twice gives the original")
def test_reverse(lst: list[int]) -> bool:
return list(reversed(list(reversed(lst)))) == lst
if __name__ == "__main__":
import sys
sys.exit(0 if check(test_reverse) else 1)
Run directly:
python my_tests.py
Documentation
Full documentation and tutorials at Read The Docs.
Usage Guide
CLI Test Runner
# Run all tests in ./tests directory
minigun --time-budget 30
# Run tests from a different directory
minigun --time-budget 60 --test-dir my_tests
# Run specific test modules
minigun --time-budget 45 --modules my_tests other_tests
# List available test modules
minigun --list-modules
# Quiet mode (for CI/CD)
minigun --time-budget 60 --output quiet
# JSON output (for automation)
minigun --time-budget 30 --output json
# Reproduce a failing run
minigun --time-budget 30 --seed 42
The CLI discovers Python files in the test directory that export a module-level spec: Spec. Every property in every module is evaluated and reported. The time budget is shared between properties in proportion to how many attempts their input domains are worth, and time a property leaves unspent flows to the properties after it.
Every run is seeded. When a property fails, the seed is printed so the exact run can be replayed with --seed, and each property draws from its own source derived from that seed, so a property's samples never depend on what else ran.
Running Programmatically
from minigun.orchestrator import OutputMode, RunConfig, TestModule, run
from minigun.specify import prop
@prop("your property")
def my_property(x: int) -> bool:
return x + 0 == x
if __name__ == "__main__":
import sys
config = RunConfig(time_budget=30.0, output=OutputMode.QUIET)
sys.exit(0 if run(config, [TestModule("my_tests", my_property)]) else 1)
For structured outcomes without a reporter, drive minigun.specify.evaluate directly; the tutorial's "Running specifications programmatically" section shows how.
Writing Tests
Basic Properties
from minigun import prop
@prop("addition is commutative")
def test_add_commute(x: int, y: int) -> bool:
return x + y == y + x
Custom Generators
from minigun import prop, context, generate as g
@context(g.int_range(1, 100), g.int_range(1, 100))
@prop("division reverses multiplication")
def test_div(x: int, y: int) -> bool:
return (x * y) // y == x
Combining Properties
from minigun import prop, check, conj, neg
@prop("property 1")
def test_1(x: int) -> bool:
return x + 0 == x
@prop("this law is false and a counterexample must be found")
def test_2(x: int) -> bool:
return x * 2 == x
# Check both together; neg holds when its term is refuted
success = check(conj(test_1, neg(test_2)))
Randomness Inside a Law
Annotate a parameter with random.Random to receive a source that is reproducible from the run seed:
import random
from minigun import prop
@prop("shuffling preserves the elements")
def test_shuffle(xs: list[int], rng: random.Random) -> bool:
shuffled = list(xs)
rng.shuffle(shuffled)
return sorted(shuffled) == sorted(xs)
FAQ
Q: What's a good time budget?
A: Start with 30-60 seconds for quick feedback. Use 2-5 minutes for thorough testing in CI/CD.
Q: How do I test larger input spaces?
A: Increase the time budget. Properties over unbounded domains absorb the extra time, up to 10000 attempts each per run.
Q: Can I customize test generation?
A: Yes, use the @context decorator with generators from minigun.generate, or write your own generator and shrinker. See the tutorial for details.
Q: How do I reproduce a failing run?
A: Every failing run prints its seed. Pass it back with minigun --seed <n> (or check(spec, seed=n)) to replay the exact same generation.
Q: My property passed but was it tested?
A: A property whose generators discard most of their draws (for example an over-restrictive g.filter) fails with a message saying how many attempts were discarded, rather than passing silently.
Real-World Usage
The following projects use Minigun for testing:
If you have used Minigun for testing a public project, please file an issue with a link to add it to this list.
Release files for minigun-soren-n 4.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| minigun_soren_n-4.0.0.tar.gz | 59.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| minigun_soren_n-4.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 120.9 kB
Release files / minigun_soren_n-4.0.0.tar.gz
| Download URL | minigun_soren_n-4.0.0.tar.gz |
|---|---|
| Size | 59.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c24c5ae575d2f3226d14e8e599c6c7d8cf370254857fe45f492b0780b3eb1b11
|
|
BLAKE2b-256 checksum How to use checksums |
a6da91e776abd5960a7e7eaaa1091a79cd1e89d38df8e57957fb659222e7836c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / minigun_soren_n-4.0.0-py3-none-any.whl
| Download URL | minigun_soren_n-4.0.0-py3-none-any.whl |
|---|---|
| Size | 61.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
357b16f1e2e762f98d7c7a3ac31d7da380b692da73ce2a38f0cc5f1d4a5df592
|
|
BLAKE2b-256 checksum How to use checksums |
3a8945a1445c7bf583fa6eab9035b059ef3cf5eb12f051bcdbb5a706403a9f51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|