The Python test runner built for precision.
▸ Zero plugins. Full arsenal.
Test Junkie is a Python test runner and test framework with built-in parallel test execution, exception-aware
retries, multi-layer parametrization, event listeners and HTML/XML/JSON reports — everything in one package, no plugins.
Use it from Python with Runner or from the terminal with the tj command.
Not a wrapper around
unittest. Every capability below ships with the library — no plugins to install, no config files to write, no external orchestrators.
- Decorator-based:
@Suiteand@teston plain Python classes — no base class to extend, no config file to maintain - Lifecycle hooks:
@beforeClass,@afterClass,@beforeTest,@afterTeston the class, plus@beforeGroup/@afterGroup(via@GroupRules) that run once around a named set of suites - Built-in parallel execution: separate thread limits for suites (
-S/suite_multithreading_limit) and tests (-T/test_multithreading_limit);parallelized=Falsekeeps a suite or test out of the parallel pool, andconflicts_with=[...]stops specific suites or tests from ever overlapping - Retries:
retry=Non a suite or a test — only the failing parameter variants re-run;retry_on=[ConnectionError]retries just infrastructure noise (subclasses included),no_retry_on=[AssertionError]makes real bugs surface immediately;tj run --retry N/--no-retryoverride retries for one run - Rerun:
tj run --rerun report.jsonruns again only what didn't pass in an earlier run, down to the failing parameter and suite parameter - Multi-layer parametrization:
@Suite(parameters=[...])×@test(parameters=[...])= every combination, each variant tracked and retried on its own; parameters can be a function that builds the list at run time Rulesfor shared setup/teardown — definebefore_class/before_test/ … once, attach with@Suite(rules=...)to as many suites as you like- First-class
owner,feature,component,tagsandpriority— filter withtj run --owners,--features,--components,--tags-any/--tags-all(tags), orowners=/features=/components=/tag_config=inRunner.run() - Execution order per suite:
@Suite(order=TestOrder.RANDOM)(orALPHABETICAL,PRIORITY_ASC,PRIORITY_DESC) — e.g. shuffle one suite to surface order dependencies; the run prints the seed, andtj run --seed Nrepeats that exact order - Skipping:
skip=Trueor a function deciding at run time on any suite or test, andshortcuts.skip("reason")from inside a running test - Live event listeners: subclass
Listener, override any of its 21 events (on_failure,on_success,on_class_skip, …) and attach it with@Suite(listener=...) - Results as objects:
runner.get_executed_suites()returns typed suite/test objects with status, timing, retries, exceptions and metadata;Meta.update(...)adds to a test's metadata while it runs - Reports: HTML (with CPU and memory graphs when
monitor_resources=True/tj run -m), JUnit XML and JSON, written to the files or folders you pass ashtml_report=/xml_report=/json_report=;runner.run()also returns the results (.get_basic_report()has the counts per status and suite) - CLI:
tj run,tj audit,tj config,tj version— run and inspect suites without writing a runner script; settings can live in a projecttj.cfgorpyproject.toml;tj runexits non-zero on failures, errors or when nothing ran and marks failures on GitHub pull requests, ready for CI
And more →
pip install test-junkie
from test_junkie.decorators import Suite, test, beforeClass
from test_junkie.runner import Runner
@Suite(feature="Login", owner="qa-team")
class LoginSuite:
@beforeClass()
def open_session(self):
self.session = {"user": "demo"}
@test(component="auth", tags=["smoke"], retry=2)
def valid_login(self):
assert self.session["user"] == "demo"
@test(component="auth", parameters=["", "wrong-password"])
def invalid_login(self, parameter):
assert parameter != "correct-password"
if __name__ == "__main__":
Runner([LoginSuite], html_report="report.html").run(test_multithreading_limit=4)Or from the terminal: tj run -s path/to/tests -T 4 --html-report report.html
Python 2.7: the last release with Python 2.7 support is
0.8a.8. All versions from0.9a0onwards require Python 3.
Key takeaways (median of 30 runs; every run verified to have passed all of its tests):
- 1.4× faster than pytest, sequentially (1.97 s vs 2.74 s, 1,000 tests). Fastest of the four runners: 1.7× faster than Robot Framework (3.39 s) and just ahead of plain unittest (2.03 s).
- 6.7× faster than pytest-xdist in parallel (0.31 s vs 2.06 s, 1,000 tests, 10 workers). Tests run on threads inside one process, so there's no worker start-up cost, and no plugin to install.
- Parallelism that scales: going from 1 to 10 threads makes a 1,000-test run 6.2× faster. pytest-xdist peaks at ×1.7; pabot is slower than sequential Robot Framework at every setting.
- 6.2× smaller than pytest with the plugins for the same features (3,345 KB vs 20,680 KB installed, every dependency included). Parallel execution, parametrization, retries, listeners, ordering and HTML/XML/JSON reports ship in one package; pytest needs 9 plugins for them, which pull in 18 more packages. Robot Framework with plugins is 8,005 KB (2.4×) and covers 13 of the 19 features fully. Details: install size.
- ~1 ms framework overhead per test at 1,000 tests (+972 ms total), vs +1,028 ms for unittest, +1,741 ms for pytest and +2,395 ms for Robot Framework.
- 95 ms start-up for a 1-test run, on par with unittest (92 ms) and about a third of pytest (296 ms) or Robot Framework (308 ms).
Timings measured with test_junkie 0.9a5, pytest 9.1.1, Robot Framework 7.5 on Python 3.12.5 (Windows 11, Intel Core i9-10980HK); install sizes with test_junkie 0.9a6 and the latest plugins on 2026-10-07. Full results tables, parametrized and parallel runs, per-phase timings and the exact commands are in the full benchmark →