FAQ
Why no test classes?
ProTest doesn't support test classes. Use suites instead.
pytest:
class TestUser:
@pytest.fixture
def user(self):
return User("alice")
def test_create(self, user):
assert user.name == "alice"
def test_delete(self, user):
user.delete()
assert user.deleted
ProTest:
from protest import ProTestSuite, fixture, Use
from typing import Annotated
user_suite = ProTestSuite("User")
@fixture()
def user():
return User("alice")
user_suite.bind(user) # SUITE scope
@user_suite.test()
def test_create(u: Annotated[User, Use(user)]):
assert u.name == "alice"
@user_suite.test()
def test_delete(u: Annotated[User, Use(user)]):
u.delete()
assert u.deleted
What classes provide vs suites
| Feature | pytest classes | ProTest suites |
|---|---|---|
| Group related tests | ✓ | ✓ |
| Shared fixtures | self or class-scoped fixtures |
suite.bind(fn) |
| Setup/teardown | setup_method/teardown_method |
yield in fixtures |
| Named groups in output | ✓ | ✓ |
| Inheritance | ✓ | ✗ |
Why not inheritance?
Test inheritance creates implicit behavior that's hard to trace. When a test fails in a subclass, you need to check the parent class, grandparent class, and any mixins to understand what's happening.
ProTest favors explicit composition: if two suites need the same fixtures, import and use them explicitly.
from myapp.fixtures import database, user
@api_suite.test()
def test_api(
db: Annotated[Database, Use(database)],
user: Annotated[User, Use(user)],
):
...
@admin_suite.test()
def test_admin(
db: Annotated[Database, Use(database)],
user: Annotated[User, Use(user)],
):
...
You can see exactly what each test uses. No hunting through class hierarchies.
Why can't fixtures use From()?
From() is only allowed in tests, not fixtures. This is intentional.
If fixtures could use From(), you'd get hidden cartesian products:
# Hypothetical - NOT SUPPORTED (decorator syntax doesn't exist either)
# If this were possible:
@fixture()
def db(engine: Annotated[str, From(ENGINES)]): # 3 engines
...
session.bind(db)
@fixture()
def user(role: Annotated[str, From(ROLES)], db: ...): # 2 roles
...
session.bind(user)
@session.test()
def test_perms(method: Annotated[str, From(METHODS)], user: ...): # 4 methods
...
# Would run 3 × 2 × 4 = 24 times with no visibility in the test!
Instead, use factories and keep parameterization visible in the test:
@session.test()
def test_perms(
engine: Annotated[str, From(ENGINES)],
role: Annotated[str, From(ROLES)],
method: Annotated[str, From(METHODS)],
db_factory: Annotated[FixtureFactory[DB], Use(database)],
user_factory: Annotated[FixtureFactory[User], Use(user)],
):
db = await db_factory(engine_type=engine)
user = await user_factory(role=role, db=db)
# You SEE it's 3×2×4 = 24 tests
Why doesn't ProTest capture subprocess output automatically?
ProTest captures print() and logging automatically, but subprocess output goes directly to OS file descriptors (fd
1/2), bypassing Python's sys.stdout.
In ProTest's async-concurrent architecture, all tests run in the same process sharing the same file descriptors. There's no way to attribute subprocess output to a specific test when multiple tests run concurrently.
Solution: use the Shell helper
from protest import Shell
@suite.test()
async def test_ffmpeg_conversion() -> None:
result = await Shell.run(["ffmpeg", "-i", "input.mp4", "output.webm"])
assert result.success
# stdout/stderr automatically captured and shown on failure
The Shell helper:
- Runs subprocesses with isolated pipes (no fd sharing issues)
- Automatically prints output for ProTest to capture
- Works safely with concurrent tests (-n 4)
- Supports timeout, working directory, environment variables
With shell features (pipes, &&, etc.):
@suite.test()
async def test_pipeline() -> None:
result = await Shell.run("cat file.txt | grep pattern", shell=True)
assert result.success
See examples/subprocess_capture/session.py for complete examples and Built-ins for full API.