🔬 Flaky Test Detector
Find the tests you can't trust — before they find you.
Run your test suite N times, catch inconsistent results, and get a ranked report of which tests are lying to you.
The Problem
A flaky test is a test that sometimes passes and sometimes fails — with no code changes between runs.
You've seen this before:
- CI fails. You re-run it. It passes. You shrug and merge.
- Your test suite is red. You check the diff. Nothing changed.
- You start ignoring red builds because they're "probably just flaky."
This is how teams stop trusting their test suites. Flaky tests cost real engineering hours and mask real bugs.
Flaky Test Detector (flakydetect) solves this by running your test suite N times and telling you exactly which tests are inconsistent and how often they fail.
Quick Start
# Install
pip install flakydetect
# Run your test suite 10 times and find the flakes
flaky-detect --cmd "pytest tests/" --runs 10
Example output:
🔬 flaky-detect v0.1.0
Command: pytest tests/
Runs: 10
╭─────────────────────────────────────────────────────────────╮
│ ⚠️ Flaky Tests │
├──────────────────────────────────┬────────────┬─────────────┤
│ Test Name │ Flake Rate │ Pass / Fail │
├──────────────────────────────────┼────────────┼─────────────┤
│ tests/test_auth.py::test_timeout │ 40.0% │ 6 / 4 │
│ tests/test_cache.py::test_race │ 20.0% │ 8 / 2 │
│ tests/test_api.py::test_retry │ 10.0% │ 9 / 1 │
╰──────────────────────────────────┴────────────┴─────────────╯
✅ Stable: 37/40 tests passed consistently.
╭────────────────────────────────╮
│ VERDICT: ISSUES FOUND │
│ 3 flaky tests | 0 broken │
╰────────────────────────────────╯
Table of Contents
- Installation
- Usage
- How It Works
- Multi-Language Support
- Understanding the Output
- Common Causes of Flaky Tests
- Project Structure (for developers)
- Contributing
- Roadmap
- License
Installation
Requirements: Python 3.8+
pip install flakydetect
Or with pipx (installs in isolated environment, keeps your global Python clean):
pipx install flakydetect
From source (for development):
git clone https://github.com/ZachR-C/flaky-test-detector.git
cd flaky-test-detector
pip install -e ".[dev]" # -e = editable install, [dev] = includes dev tools
Usage
Basic Usage
# Run pytest 10 times (default)
flaky-detect --cmd "pytest tests/"
# Run 20 times for higher statistical confidence
flaky-detect --cmd "pytest tests/" --runs 20
# Save a JSON report
flaky-detect --cmd "pytest tests/" --runs 15 --output flaky-report.json
# Run in a specific directory
flaky-detect --cmd "pytest tests/" --runs 10 --dir /path/to/myproject
All Options
Usage: flaky-detect [OPTIONS]
Options:
-c, --cmd TEXT The test command to run. [required]
-n, --runs INTEGER Number of times to run. [1-1000] [default: 10]
-o, --output PATH Save report as JSON to this path.
-t, --threshold FLOAT Min flake % to flag. [0.0-100.0] [default: 5.0]
-d, --dir PATH Working directory for the command.
--parser TEXT Parser: auto|junit_xml|pytest_text [default: auto]
--timeout INTEGER Seconds per run before kill. [default: 300]
-v, --verbose Show failure messages and debug info.
-q, --quiet Only show the final verdict.
--version Print version and exit.
--help Show this message and exit.
Examples
# Pytest with verbose output
flaky-detect --cmd "pytest tests/ -v" --runs 10
# Python's unittest module
flaky-detect --cmd "python -m unittest discover tests/" --runs 10 --parser pytest_text
# Java with JUnit (requires maven generating surefire XML)
flaky-detect --cmd "mvn test -q" --runs 10 --parser junit_xml
# Set a high threshold — only flag tests that flake more than 20% of the time
flaky-detect --cmd "pytest tests/" --runs 20 --threshold 20.0
# Quiet mode for CI — suppress all output, use exit code only
flaky-detect --cmd "pytest tests/" --runs 10 --quiet
# Full verbose output with failure messages
flaky-detect --cmd "pytest tests/" --runs 15 --verbose
CI/CD Integration
Exit codes:
| Code | Meaning |
|---|---|
0 |
All tests stable — no issues found |
1 |
Tool error (command failed to start, etc.) |
2 |
Flaky or always-failing tests found |
Use this in GitHub Actions:
# .github/workflows/flaky-check.yml
- name: Run flaky test detection
run: |
flaky-detect \
--cmd "pytest tests/" \
--runs 10 \
--output flaky-report.json
# Exit code 2 = flaky tests found. Whether to fail CI is up to you.
Full reusable workflow: See .github/workflows/flaky-check.yml — copy it into your own repo to add weekly flake detection.
How It Works
The architecture is deliberately simple and extensible:
┌──────────────┐ ┌──────────────────┐ ┌─────────────┐ ┌──────────────┐
│ CLI (cli.py)│─────▶│ Runner (runner.py)│─────▶│ Parsers │─────▶│ Aggregator │
│ │ │ │ │ (parsers/) │ │(aggregator.py│
│ Parse args │ │ Run command N │ │ │ │ │
│ Set up logs │ │ times via │ │ JUnit XML │ │ Compute per- │
│ Orchestrate │ │ subprocess │ │ ─or─ │ │ test stats │
│ pipeline │ │ │ │ Pytest text │ │ (pass counts,│
└──────────────┘ │ Capture output, │ │ │ │ flake rates) │
│ exit codes, │ │ Returns │ └──────┬───────┘
│ timing │ │ List of │ │
└──────────────────┘ │ TestResult │ ▼
└─────────────┘ ┌──────────────┐
│ Reporter │
│ (report.py) │
│ │
│ Terminal │
│ output (rich│
│ or plain) │
│ + JSON file │
└──────────────┘
-
Runner — Executes your test command N times using Python's
subprocessmodule. For pytest, it injects--junitxmlto get structured XML output. -
Parsers — Convert raw output into structured
TestResultobjects. The abstractBaseParserdefines the interface; concrete implementations (JUnitXMLParser,PytestTextParser) handle specific formats. -
Aggregator — Groups results by test name across all runs and computes statistics: pass count, fail count, flake rate, classification (flaky / broken / stable).
-
Reporter — Formats the final
FlakeReportfor terminal display (usingrich) and JSON export.
Multi-Language Support
Because we parse JUnit XML (a format supported by virtually every test framework), flakydetect supports multiple languages without language-specific parsers:
| Framework | Language | How to generate JUnit XML |
|---|---|---|
| pytest | Python | Built-in: pytest --junitxml=output.xml |
| unittest | Python | pip install pytest then run via pytest |
| JUnit 4/5 | Java | Built-in with Maven Surefire / Gradle |
| TestNG | Java | Built-in |
| GoogleTest | C++ | --gtest_output=xml:output.xml |
| CTest | C/C++ | ctest --output-on-failure -T test |
| jest | JavaScript | npm install jest-junit, configure reporters |
Understanding the Output
Flaky vs. Broken
flakydetect distinguishes between two kinds of problem:
| Category | Definition | What to do |
|---|---|---|
| ⚠️ Flaky | Sometimes passes, sometimes fails | Investigate; quarantine first |
| ❌ Broken | Fails in EVERY run (never passes) | Fix the bug — this is a real failure |
| ✅ Stable | Passes in EVERY run | Nothing to do |
A broken test is not flaky — it's just failing. flakydetect reports it separately so you can tell the difference between "this test is unreliable" and "this test has a real bug."
Flake Rate Formula
The flake rate for a test is computed as:
flake_rate = min(pass_count, fail_count) / total_runs
Why min()?
We want to capture the "minority outcome" — the direction in which the test is inconsistent. A test that passes 8/10 times has the same degree of inconsistency as one that fails 8/10 times, but flipped. Taking the minimum of passes and failures gives the fraction of runs that were the "exception."
| pass | fail | total | formula | rate |
|---|---|---|---|---|
| 10 | 0 | 10 | min(10,0)/10 | 0.0% |
| 9 | 1 | 10 | min(9,1)/10 | 10.0% |
| 7 | 3 | 10 | min(7,3)/10 | 30.0% |
| 5 | 5 | 10 | min(5,5)/10 | 50.0% |
| 0 | 10 | 10 | min(0,10)/10 | 0.0% |
Note that 0% rate means either "always passing" OR "always failing" — use is_always_failing in the JSON output to tell the difference.
Common Causes of Flaky Tests
Understanding WHY tests are flaky helps you fix them. Here are the most common causes with examples from the included examples/demo_project/:
1. Randomness without a fixed seed
# ❌ Flaky: random value might be below 0.3
def test_score_is_high():
score = get_random_score()
assert score > 0.3
# ✅ Fixed: test the range, not a specific value
def test_score_is_valid():
score = get_random_score()
assert 0.0 <= score <= 1.0
Fix: Either mock the random function, set random.seed(42) before the test, or test properties rather than specific values.
2. Timing assumptions
# ❌ Flaky: 30ms may not be enough on a slow CI runner
def test_background_task():
task.start()
time.sleep(0.03)
assert task.result == expected # May fail if task isn't done
# ✅ Fixed: wait for a signal, not a fixed time
def test_background_task():
task.start()
completed = task.wait(timeout=5.0) # Uses threading.Event
assert completed
assert task.result == expected
Fix: Use synchronization primitives (threading.Event, asyncio.Event, queue.Queue) instead of fixed sleeps.
3. Shared mutable state between tests
# ❌ Flaky: test_b is affected by test_a if test_a runs first
items = [] # Module-level mutable state
def test_a():
items.append("hello")
assert len(items) == 1
def test_b():
assert len(items) == 0 # Fails if test_a ran first!
# ✅ Fixed: each test creates its own fresh state
def test_a():
items = []
items.append("hello")
assert len(items) == 1
def test_b():
items = [] # Fresh, independent list
assert len(items) == 0
Fix: Avoid module-level mutable state. Use @pytest.fixture with autouse=True to reset shared state before each test.
4. Test order dependency
Similar to shared state — test B relies on test A having run first. pytest doesn't guarantee run order by default (and plugins like pytest-randomly actively randomize it).
Fix: Each test must set up its own preconditions. Use fixtures.
Project Structure (for developers)
The codebase is organized around the Separation of Concerns principle — each module has one clearly defined job:
flaky-test-detector/
├── src/
│ └── flakydetect/
│ ├── __init__.py # Package metadata and version
│ ├── cli.py # CLI argument parsing and orchestration
│ ├── runner.py # Execute test commands (subprocess)
│ ├── aggregator.py # Combine run results into statistics
│ ├── report.py # Format and output the final report
│ ├── models.py # Data structures (TestResult, FlakeReport, etc.)
│ └── parsers/
│ ├── __init__.py # Factory: get_parser() selects the right parser
│ ├── base.py # Abstract base class (the interface contract)
│ ├── junit_xml.py # Parses JUnit XML (Python, Java, C++, JS)
│ └── pytest_text.py # Parses pytest stdout (fallback)
├── tests/
│ ├── test_models.py # Unit tests for data models
│ ├── test_parsers.py # Unit tests for parsers
│ ├── test_aggregator.py # Unit tests for the aggregation logic
│ └── fixtures/
│ └── sample_junit_output.xml
├── examples/
│ └── demo_project/
│ ├── test_stable.py # Stable tests (should never flake)
│ ├── test_flaky_random.py # Flaky due to randomness
│ └── test_flaky_sleep.py # Flaky due to timing assumptions
├── .github/
│ └── workflows/
│ ├── ci.yml # CI: lint + tests + type checking
│ └── flaky-check.yml # Reusable workflow for users to copy
├── docs/
├── pyproject.toml # Package config, dependencies, tool settings
└── README.md
Adding a new parser (e.g., for Mocha/JavaScript or Go's gotestsum):
- Create
src/flakydetect/parsers/mocha_json.py - Define a class that inherits from
BaseParser - Implement the
parse()method - Register it in
get_parser()inparsers/__init__.py
That's it — no other files need to change.
Contributing
Contributions are very welcome! This project is a great place to make your first open source contribution.
Setup
git clone https://github.com/ZachR-C/flaky-test-detector.git
cd flaky-test-detector
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -e ".[dev]"
Running tests
pytest tests/ -v
Running the linter
ruff check src/ tests/
black src/ tests/
Good first issues
- Add a parser for a new test framework (Go's gotestsum, Mocha, Jest)
- Add an
--htmlflag to generate an HTML report - Add a
--badgeflag to generate a shields.io-compatible badge - Improve the output formatting
- Add more examples to
examples/demo_project/
Roadmap
v0.1.0 (current):
- Core run-N-times engine
- JUnit XML parser (pytest, JUnit, GoogleTest)
- pytest stdout fallback parser
- Rich terminal output
- JSON report export
- CI exit codes
- GitHub Actions integration examples
v0.2.0 (planned):
- Historical tracking (SQLite, track trends over time)
- HTML report generation
- Statistical confidence scoring (Wilson interval)
-
--badgeflag (generate a shields.io badge URL) - Pre-commit hook integration
v0.3.0 (planned):
- Quarantine mode (
--quarantineflag: skip known flaky tests in CI) - GitHub PR comment integration
- Slack/webhook notifications
License
MIT License — see LICENSE for details.
Free for personal and commercial use. Attribution appreciated but not required.
Built by Zachary Cherney — a computer science student who got tired of spurious CI failures.
If this saved you time, please ⭐ star the repo — it helps others find it.
Metadata
Release files for flakydetect 0.1.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 | |
|---|---|---|---|
| flakydetect-0.1.0.tar.gz | 63.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| flakydetect-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 109.4 kB
Release files / flakydetect-0.1.0.tar.gz
| Download URL | flakydetect-0.1.0.tar.gz |
|---|---|
| Size | 63.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2f7939213a2c8bfd8ce2ba335b9126f039920a7ff113e5ae5bf60818323ad0f1
|
|
BLAKE2b-256 checksum How to use checksums |
ddbb939c3af592a325c8c140ceee299c01423f306ee89791fe3e47304ce98d11
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.6
|
Release files / flakydetect-0.1.0-py3-none-any.whl
| Download URL | flakydetect-0.1.0-py3-none-any.whl |
|---|---|
| Size | 45.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
19b5c47e7c7b434b16ca29ca9eecedc83edd453364f2d971122b294e85b023ce
|
|
BLAKE2b-256 checksum How to use checksums |
93b54fb0c6bd2cfd28b3a6a80fb7307c2e1600dd07c68e32366f2981d1b74a59
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.6
|