Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

voci

A concurrent test runner for async Python. voci runs your whole suite in one process on one event loop, with every test as a concurrent asyncio task. For a suite that spends its time waiting — on a database, on an ASGI app, on a network stub — that turns wall-clock time from the sum of your tests into roughly the slowest one, with a single connection pool and a single set of imports behind it.

Tests declare what they need as parameter defaults, the way FastAPI routes do. There is no conftest.py and no name-based lookup: a fixture is a function you import, so "go to definition" works, renames are safe, and a typo is an ImportError at collection rather than a mystery at run time. When a fixture is a side effect no test needs the value of, a module — or a package, for everything under it — declares it once: voci.use(reset_cache), the way a FastAPI router declares dependencies for every route on it. Assertions keep the introspection you already know — voci vendors pytest's assertion rewriter, so assert a == b still prints a real diff.

# tests/fixtures.py
from typing import Annotated

import voci
from voci import Depends


@voci.fixture(scope="session")
async def engine() -> AsyncIterator[AsyncEngine]:
    engine = create_async_engine("sqlite+aiosqlite:///./test.db")
    yield engine
    await engine.dispose()


@voci.fixture()
async def session(engine: Annotated[AsyncEngine, Depends(engine)]) -> AsyncIterator[AsyncSession]:
    """A transaction per test, always rolled back — so tests share one engine safely."""
    async with engine.connect() as conn:
        transaction = await conn.begin()
        yield AsyncSession(bind=conn)
        await transaction.rollback()


# tests/test_users.py
from typing import Annotated

from voci import Depends

from tests.fixtures import session


async def test_create_user(db: Annotated[AsyncSession, Depends(session)]) -> None:
    db.add(User(email="alice@example.com"))
    await db.flush()

    assert await db.scalar(select(func.count()).select_from(User)) == 1
$ voci
PASS  tests/test_users.py                        10 tests  Σ 3.84s
PASS  tests/test_orders.py                       12 tests  Σ 7.29s

22 tests · 22 passed · 1.14s wall (9.8x concurrency)

The last line is the one to watch: the multiplier is what running concurrently bought you over the sum of every test's own duration (each file's Σ, above).

Install

Python 3.13+. The core package has no dependencies.

uv pip install voci          # or: pip install voci

Configure

Everything is optional. voci reads [tool.voci] from the nearest pyproject.toml:

[tool.voci]
testpaths = ["tests"]
concurrency = 16                   # tests in flight at once
timeout = 60                       # per-test budget, in seconds
env = { ENVIRONMENT = "test" }

CLI flags win over config, which wins over the defaults:

voci                              # run everything
voci tests/test_users.py          # run one file
voci tests/test_users.py::test_create[admin]   # run one test, or one of its cases
voci -k "users and not slow"      # select by substring of the test id
voci -m "smoke"                   # select by @voci.tag
voci --serial                     # exactly serial — the first debugging step
voci -x                           # stop at the first failure, cancelling what is in flight
voci --durations 10               # the slowest tests, to tune --concurrency by
voci --loop-watchdog 10           # how long the loop may block before voci names the call
voci --collect-only               # print the ids that would run, and stop
voci -s                           # live, id-prefixed output instead of captured

Where to go next

  • examples/ — three worked suites: a FastAPI + async SQLAlchemy CRUD API, a pure-async library, and shared resources under concurrency. Each is runnable.
  • ROADMAP.md — what isn't built yet, and what's next.
  • plans/rationale.md — why voci is shaped the way it is. Read this before changing anything load-bearing.

Status

Pre-release, ahead of v0.1. The core is real and exercised by voci's own suite and the three examples: discovery, dependency injection with four scopes and inverted teardown, concurrent execution with per-test timeouts, capture, assertion introspection, selection, and the reporter. Fair scheduling around shared resources, the migration tool, and machine-readable reports are still ahead — see ROADMAP.md. The public API is not frozen yet.

Trade-offs

voci is not pytest and does not aim to be compatible with it. There is no plugin ecosystem and no hook system — dependency injection is the extension point. Adopting it means rewriting your fixture wiring, and it requires Python 3.13+. Linux and macOS are supported; Windows is best-effort.

unittest.mock keeps working, at a price: mock.patch writes to a module or class, which every concurrently running test would see, so voci runs a patch-decorated test alone and reports what that cost. A patch opened inside a test body fails that test unless it is marked @voci.solo. examples/02-async-library walks the alternatives.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

voci-0.1.0rc2.tar.gz (980.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

voci-0.1.0rc2-py3-none-any.whl (257.9 kB view details)

Uploaded Python 3

File details

Details for the file voci-0.1.0rc2.tar.gz.

File metadata

  • Download URL: voci-0.1.0rc2.tar.gz
  • Upload date:
  • Size: 980.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for voci-0.1.0rc2.tar.gz
Algorithm Hash digest
SHA256 8a2a35c6ca955785e1acafd46d899c2513fdee8c40ed1349936081a4efbdaaa0
MD5 d9db1a57fb8555a8d0279e65fe4aeda2
BLAKE2b-256 ce8798b637d04e27da07f90be62a31f3d0795db8a98f613d1b075a16f9db3532

See more details on using hashes here.

Provenance

The following attestation bundles were made for voci-0.1.0rc2.tar.gz:

Publisher: release.yml on Ddedalus/voci

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file voci-0.1.0rc2-py3-none-any.whl.

File metadata

  • Download URL: voci-0.1.0rc2-py3-none-any.whl
  • Upload date:
  • Size: 257.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for voci-0.1.0rc2-py3-none-any.whl
Algorithm Hash digest
SHA256 29829ec0bcaa68daeb2817326c61d4e77f3bec6f92dce0d520a83062c8678cf6
MD5 2eb4c87d43cd3e691e9133f9a2ca71bc
BLAKE2b-256 76b6cc80f3250cf1baae2aa6678d4f5f3eb6a19022094f18a8d700a7b537ed9c

See more details on using hashes here.

Provenance

The following attestation bundles were made for voci-0.1.0rc2-py3-none-any.whl:

Publisher: release.yml on Ddedalus/voci

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0rc2 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page