# inline-tests > Rust-like inline testing in Python, without the compiler. inline-tests is a pytest plugin for colocating tests with implementation, Rust-style. Use @test or @it decorators to mark functions as tests, then run with `itest`. Tests live next to the code they test. Key features: @test/@it decorators, AST-based collection, zero overhead when not testing, full pytest compatibility including fixtures, parametrize, async, and all plugins. # Getting Started # [Installation](#installation) ## [Recommended: uv tool](#recommended-uv-tool) ``` uv tool install inline-tests ``` This gives you `itest` globally. For one-off use: ``` uvx inline-tests ``` ## [As a project dependency](#as-a-project-dependency) ``` uv add inline-tests --group dev ``` Or with pip: ``` pip install inline-tests ``` ## [With extras](#with-extras) inline-tests bundles common testing dependencies as optional extras: ``` # Everything uv tool install inline-tests[full] # Async testing (pytest-asyncio, anyio, pyleak) uv tool install inline-tests[async] # Core testing extras (async + mock + coverage) uv tool install inline-tests[essentials] ``` See [Extras](https://oss.dedaluslabs.ai/inline-tests-python/reference/extras/index.md) for the full list. ## [Verify installation](#verify-installation) ``` itest --version ``` You should see the pytest version. inline-tests is a pytest plugin, so it uses pytest's CLI. # [Quick Start](#quick-start) ## [1. Write a test](#1-write-a-test) Add `@test` to any function in your source code: ``` # math_utils.py from inline_tests import test def add(a, b): return a + b @test def addition_works(): assert add(2, 2) == 4 assert add(-1, 1) == 0 ``` ## [2. Run it](#2-run-it) ``` itest ``` That's it. The test runs. Your normal code stays the same. ## [3. Mix with regular tests](#3-mix-with-regular-tests) inline-tests is additive. Your `test_*.py` files still work: ``` # Run inline tests itest # Run regular pytest tests pytest # Run both itest && pytest ``` Alias suggestion Add to your shell config: ``` alias t="itest && pytest" ``` ## [Next steps](#next-steps) - [The Decorator](https://oss.dedaluslabs.ai/inline-tests-python/usage/decorator/index.md) - @test options and patterns - [pytest Integration](https://oss.dedaluslabs.ai/inline-tests-python/usage/pytest-integration/index.md) - Fixtures, parametrize, async # Usage # [The Decorator](#the-decorator) ## [Why a decorator?](#why-a-decorator) You might wonder: why not just name functions `test_*` and let pytest collect them? The decorator makes inline tests **opt-in**. Without `--inline-tests`, the plugin doesn't import your source files at all. It scans for the `@test` decorator via AST first. No decorator? The file is skipped. No import, no overhead, no accidental test runs. This matters: - **You control when inline tests run.** `pytest` runs your regular test suite. `itest` adds inline tests. Two modes, explicit choice. - **No pollution.** Your CI can run `pytest tests/` without touching source files. - **Naming freedom.** Write `rejects_empty_input()` instead of `test_rejects_empty_input()`. - **Method-level granularity.** Put `@test` on methods in regular classes without needing a `TestFoo` class. ## [Basic usage](#basic-usage) ``` from inline_tests import test @test def my_test(): assert True ``` The decorator marks the function. Nothing else changes. The function still exists at runtime and can be called normally. ## [@it alias](#it-alias) For BDD-style naming: ``` from inline_tests import it @it def should_return_empty_list_for_no_input(): assert process([]) == [] ``` `it` is literally `test`. Same function, different name. ## [With reason](#with-reason) Document why a test exists: ``` @test(reason="regression: issue #42") def handles_unicode_input(): assert parse("é") == "é" ``` The reason is stored but not currently displayed. Future versions may surface it in reports. ## [Test classes](#test-classes) Group related tests: ``` class ValidationTests: @test def rejects_empty(self): assert validate("") is False @test def accepts_valid(self): assert validate("hello") is True ``` Only the methods need `@test`. The class is just for organization. ## [Async tests](#async-tests) Works with pytest-asyncio: ``` @test async def fetches_data(): result = await fetch("https://api.example.com") assert result.status == 200 ``` Requires `inline-tests[async]` or `pytest-asyncio` installed separately. ## [Production considerations](#production-considerations) Unlike Rust's `#[cfg(test)]`, Python can't compile out decorated functions. They exist at runtime. For zero overhead in production: ``` try: from inline_tests import test except ImportError: test = lambda f=None, **_: f if f else (lambda x: x) ``` Or simply keep inline-tests as a dev dependency. The decorator itself costs nothing if you don't run `itest`. # [CLI](#cli) ## [The itest command](#the-itest-command) `itest` is a thin wrapper around pytest with `--inline-tests` enabled: ``` itest ``` Equivalent to: ``` pytest --inline-tests ``` ## [Passing arguments](#passing-arguments) All pytest arguments work: ``` # Verbose output itest -v # Run specific file itest src/auth/login.py # Run specific test itest -k "test_name" # Stop on first failure itest -x # Show print output itest -s ``` ## [Common patterns](#common-patterns) ``` # Run only inline tests in src/ itest src/ # Run with coverage itest --cov=src # Run in parallel itest -n auto # Run specific markers itest -m "not slow" ``` ## [Exit codes](#exit-codes) Standard pytest exit codes: | Code | Meaning | | ---- | -------------------------- | | 0 | All tests passed | | 1 | Some tests failed | | 2 | Test execution interrupted | | 3 | Internal error | | 4 | pytest usage error | | 5 | No tests collected | ## [Production builds](#production-builds) **Ship as-is.** The `@test` decorator is a no-op at runtime. Tests never execute unless you run `itest`. Zero overhead. **Strip tests.** Remove `@test` functions before packaging: ``` itest strip src/ -o dist/ ``` ### [Build system integration](#build-system-integration) **uv_build** — No transform hooks. Strip separately, then build: ``` itest strip src/ -o build/src cd build && uv build ``` **hatchling** — Supports [build hooks](https://hatch.pypa.io/latest/plugins/build-hook/reference/). A future `inline-tests-hatch` plugin could strip automatically during `hatch build`. Both produce identical results. Choose based on your existing toolchain. # [pytest Integration](#pytest-integration) inline-tests is a pytest plugin. Everything pytest offers works here. ## [Fixtures](#fixtures) ``` from inline_tests import test @test def writes_file(tmp_path): f = tmp_path / "test.txt" f.write_text("hello") assert f.read_text() == "hello" ``` All built-in fixtures work: `tmp_path`, `capsys`, `monkeypatch`, `request`, etc. ## [Custom fixtures](#custom-fixtures) Define fixtures in `conftest.py` as usual: ``` # conftest.py import pytest @pytest.fixture def db_connection(): conn = create_connection() yield conn conn.close() ``` ``` # models.py from inline_tests import test @test def queries_database(db_connection): result = db_connection.execute("SELECT 1") assert result == 1 ``` ## [Parametrize](#parametrize) ``` import pytest from inline_tests import test @test @pytest.mark.parametrize("x,expected", [ (1, 1), (2, 4), (3, 9), ]) def squares_correctly(x, expected): assert x * x == expected ``` ## [Markers](#markers) ``` import pytest from inline_tests import test @test @pytest.mark.slow def long_running_test(): ... @test @pytest.mark.skip(reason="not implemented") def future_test(): ... ``` Run with markers: ``` itest -m "not slow" ``` ## [Async](#async) With `pytest-asyncio` (included in `inline-tests[async]`): ``` from inline_tests import test @test async def async_test(): result = await some_async_function() assert result is not None ``` ## [Plugins](#plugins) All pytest plugins work. Popular ones: - **pytest-mock** - `mocker` fixture for mocking - **pytest-cov** - Coverage reporting - **pytest-xdist** - Parallel test execution - **hypothesis** - Property-based testing Install via extras: ``` uv tool install inline-tests[full] ``` # Reference # [API Reference](#api-reference) ## [inline_tests](#inline_tests_1) Inline tests for Python — colocate tests with implementation, Rust-style. ### [`it = test`](#inline_tests.it) ### [`MARKER = '__inline_test__'`](#inline_tests.MARKER) Attribute name set on decorated test functions/classes. ### [`__version__ = version('inline-tests')`](#inline_tests.__version__) ### [`test(obj=None, *, reason=None)`](#inline_tests.test) ``` test(obj: T) -> T ``` ``` test(*, reason: str | None = ...) -> Callable[[T], T] ``` Mark a function or class as an inline test. Source code in `src/inline_tests/__init__.py` ``` def test(obj: T | None = None, *, reason: str | None = None) -> T | Callable[[T], T]: """Mark a function or class as an inline test.""" def mark(target: T) -> T: setattr(target, MARKER, reason or True) return target return mark(obj) if obj is not None else mark ``` ## [Decorator](#decorator) ### [test](#test) ``` @test def my_test(): ... @test(reason="why this test exists") def my_test(): ... ``` Marks a function or class as an inline test. **Parameters:** | Name | Type | Required | Description | | -------- | ----- | -------- | ------------------------------------- | | `reason` | `str` | No | Documentation for why the test exists | **Returns:** The original function, unchanged except for a marker attribute. ### [it](#it) Alias for `test`. Use for BDD-style test names: ``` @it def should_handle_empty_input(): ... ``` ## [Constants](#constants) ### [MARKER](#marker) ``` MARKER: Final[str] = "__inline_test__" ``` The attribute name set on decorated functions. Used internally by the plugin for collection. ### [**version**](#version) ``` __version__: str ``` Package version string, read from package metadata. ## [Plugin hooks](#plugin-hooks) The plugin registers these pytest hooks: - `pytest_addoption` - Adds `--inline-tests` flag - `pytest_collect_file` - Collects `@test`-decorated items from non-test files - `pytest_collection_modifyitems` - Deduplicates items between collectors # [Extras](#extras) Optional testing dependencies, bundled for convenience. ## [Install syntax](#install-syntax) ``` # Single extra uv tool install inline-tests[async] # Multiple extras uv tool install inline-tests[async,mock] # All extras uv tool install inline-tests[full] ``` ## [Available extras](#available-extras) | Extra | Packages | Use case | | ---------- | ----------------------------- | ---------------------------------- | | `async` | pytest-asyncio, anyio, pyleak | Async test support, leak detection | | `mock` | pytest-mock | Mocking with `mocker` fixture | | `cov` | pytest-cov | Code coverage reporting | | `parallel` | pytest-xdist | Run tests in parallel | | `bench` | pytest-benchmark | Performance benchmarking | | `property` | hypothesis | Property-based testing | | `http` | pytest-httpx | Mock httpx requests | | `data` | faker | Generate fake test data | ## [Bundles](#bundles) | Bundle | Includes | Description | | ------------ | ---------------- | ------------------ | | `essentials` | async, mock, cov | Core testing needs | | `full` | all of the above | Everything | ## [Recommendations](#recommendations) **Starting out:** `essentials` covers most needs. **Production codebase:** `full` gives you everything, add what you use. **Minimal:** Install extras individually as needed.