Skip to content

Repository files navigation

Tests Maintenance Known Vulnerabilities PyPI version shields.io PyPI pyversions codecov Downloads License: MIT

Test Junkie

The Python test runner built for precision.
▸ Zero plugins. Full arsenal.

Test Junkie Share on X

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.

Key Features

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: @Suite and @test on plain Python classes — no base class to extend, no config file to maintain
  • Lifecycle hooks: @beforeClass, @afterClass, @beforeTest, @afterTest on 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=False keeps a suite or test out of the parallel pool, and conflicts_with=[...] stops specific suites or tests from ever overlapping
  • Retries: retry=N on 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-retry override retries for one run
  • Rerun: tj run --rerun report.json runs 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
  • Rules for shared setup/teardown — define before_class / before_test / … once, attach with @Suite(rules=...) to as many suites as you like
  • First-class owner, feature, component, tags and priority — filter with tj run --owners, --features, --components, --tags-any / --tags-all (tags), or owners= / features= / components= / tag_config= in Runner.run()
  • Execution order per suite: @Suite(order=TestOrder.RANDOM) (or ALPHABETICAL, PRIORITY_ASC, PRIORITY_DESC) — e.g. shuffle one suite to surface order dependencies; the run prints the seed, and tj run --seed N repeats that exact order
  • Skipping: skip=True or a function deciding at run time on any suite or test, and shortcuts.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 as html_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 project tj.cfg or pyproject.toml; tj run exits non-zero on failures, errors or when nothing ran and marks failures on GitHub pull requests, ready for CI

And more →

Installation

pip install test-junkie

Quick example

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 from 0.9a0 onwards require Python 3.

Performance

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 →

Getting Started

Getting Started Guide   Full Documentation


Changelog    Report a Bug

Releases

Sponsor this project

Packages

Used by

Contributors

Languages