Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ site/

# Git worktrees
.worktrees/

# uv lock file (library, not application)
uv.lock
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.4.1] - 2026-03-05

### Added

- `with bigfoot:` and `async with bigfoot:` — shorthand for `with bigfoot.sandbox():` / `async with bigfoot.sandbox():`. Both forms return the active `StrictVerifier` from `__enter__`, so `with bigfoot as v:` gives direct access to the verifier when needed (e.g. for registering custom plugins manually).

## [0.4.0] - 2026-03-05

### Added
Expand Down Expand Up @@ -78,6 +84,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Multi-OS CI matrix (Ubuntu, macOS, Windows) across Python 3.11, 3.12, and 3.13
- OIDC trusted publishing to PyPI on `v*` tags

[0.4.1]: https://github.com/axiomantic/bigfoot/releases/tag/v0.4.1
[0.4.0]: https://github.com/axiomantic/bigfoot/releases/tag/v0.4.0
[0.3.0]: https://github.com/axiomantic/bigfoot/releases/tag/v0.3.0
[0.2.0]: https://github.com/axiomantic/bigfoot/releases/tag/v0.2.0
Expand Down
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_payment_flow():
bigfoot.http.mock_response("POST", "https://api.stripe.com/v1/charges",
json={"id": "ch_123"}, status=200)

with bigfoot.sandbox():
with bigfoot:
import httpx
response = httpx.post("https://api.stripe.com/v1/charges",
json={"amount": 5000})
Expand All @@ -51,7 +51,7 @@ def test_service_calls():
payment.charge.returns({"status": "ok"})
payment.refund.required(False).returns(None) # optional mock

with bigfoot.sandbox():
with bigfoot:
result = payment.charge(order_id=42)

bigfoot.assert_interaction(payment.charge, args=(42,), kwargs={"order_id": 42})
Expand Down Expand Up @@ -79,7 +79,7 @@ def test_deploy():
bigfoot.subprocess_mock.mock_run(["git", "pull", "--ff-only"], returncode=0, stdout="Already up to date.\n")
bigfoot.subprocess_mock.mock_run(["git", "tag", "v1.0"], returncode=0)

with bigfoot.sandbox():
with bigfoot:
deploy()

bigfoot.assert_interaction(bigfoot.subprocess_mock.which, name="git")
Expand Down Expand Up @@ -114,15 +114,15 @@ def test_deploy():
def test_no_subprocess_calls():
bigfoot.subprocess_mock.install() # any subprocess.run call will raise UnmockedInteractionError

with bigfoot.sandbox():
with bigfoot:
result = function_that_should_not_call_subprocess()

assert result == expected
```

## Async Tests

`sandbox()` and `in_any_order()` both support `async with`:
`bigfoot` and `bigfoot.in_any_order()` both support `async with`:

```python
import bigfoot
Expand All @@ -131,7 +131,7 @@ import httpx
async def test_async_flow():
bigfoot.http.mock_response("GET", "https://api.example.com/items", json=[])

async with bigfoot.sandbox():
async with bigfoot:
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/items")

Expand All @@ -150,7 +150,7 @@ async def test_concurrent():
bigfoot.http.mock_response("GET", "https://api.example.com/a", json={"a": 1})
bigfoot.http.mock_response("GET", "https://api.example.com/b", json={"b": 2})

async with bigfoot.sandbox():
async with bigfoot:
async with asyncio.TaskGroup() as tg:
ta = tg.create_task(httpx.AsyncClient().get("https://api.example.com/a"))
tb = tg.create_task(httpx.AsyncClient().get("https://api.example.com/b"))
Expand All @@ -175,7 +175,7 @@ real_service = PaymentService()
payment = bigfoot.spy("PaymentService", real_service)
payment.charge.returns({"id": "mock-123"}) # queue entry: takes priority

with bigfoot.sandbox():
with bigfoot:
result1 = payment.charge(100) # uses queue entry
result2 = payment.charge(200) # queue empty: delegates to real_service.charge(200)

Expand All @@ -196,7 +196,7 @@ def test_mixed():
bigfoot.http.mock_response("GET", "https://api.example.com/cached", json={"data": "cached"})
bigfoot.http.pass_through("GET", "https://api.example.com/live")

with bigfoot.sandbox():
with bigfoot:
mocked = httpx.get("https://api.example.com/cached") # returns mock
real = httpx.get("https://api.example.com/live") # makes real HTTP call

Expand All @@ -221,13 +221,15 @@ def test_something():
svc = bigfoot.mock("MyService")
svc.call.returns("ok")

with bigfoot.sandbox():
with bigfoot:
result = svc.call()

bigfoot.assert_interaction(svc.call)
# verify_all() runs at teardown automatically
```

`with bigfoot:` is shorthand for `with bigfoot.sandbox():`. Both return the active verifier, so `with bigfoot as v:` works if you need the verifier instance directly.

An explicit `bigfoot_verifier` fixture is available as an escape hatch when you need direct access to the `StrictVerifier` object.

## HTTP Interception Scope
Expand Down Expand Up @@ -267,7 +269,8 @@ import bigfoot
bigfoot.mock("Name") # create/retrieve a named MockProxy
bigfoot.mock("Name", wraps=real) # spy: delegate to real when queue empty
bigfoot.spy("Name", real) # positional form of wraps=
bigfoot.sandbox() # context manager: activate all plugins
bigfoot # preferred sandbox shorthand: `with bigfoot:` or `async with bigfoot:`
bigfoot.sandbox() # explicit form; equivalent to `with bigfoot:`
bigfoot.assert_interaction(source, **fields) # assert next interaction; ALL assertable fields required
bigfoot.in_any_order() # relax FIFO ordering for assertions
bigfoot.verify_all() # explicit verification (automatic in pytest)
Expand Down
16 changes: 9 additions & 7 deletions docs/guides/async.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Async Usage

bigfoot supports async tests natively. `bigfoot.sandbox()` and `bigfoot.in_any_order()` both implement `__aenter__` and `__aexit__`.
bigfoot supports async tests natively. `bigfoot` and `bigfoot.in_any_order()` both implement `__aenter__` and `__aexit__`.

## async with sandbox
## async with bigfoot

Use `async with bigfoot.sandbox()` in an async test function:
Use `async with bigfoot:` in an async test function:

```python
import bigfoot
Expand All @@ -13,14 +13,16 @@ import httpx
async def test_async_http():
bigfoot.http.mock_response("GET", "https://api.example.com/data", json={"ok": True})

async with bigfoot.sandbox():
async with bigfoot:
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/data")
assert response.json() == {"ok": True}

bigfoot.assert_interaction(bigfoot.http.request, method="GET", url="https://api.example.com/data")
```

`async with bigfoot:` is shorthand for `async with bigfoot.sandbox():`. Both return the active `StrictVerifier` from `__aenter__`. `bigfoot.sandbox()` is also available as the explicit form and returns a `SandboxContext` for cases where you need to pass the context manager around.

The sync and async forms are equivalent. `SandboxContext._enter()` and `_exit()` are synchronous under the hood; the async wrapper simply delegates to them.

## ContextVar isolation
Expand All @@ -40,7 +42,7 @@ async def test_concurrent_requests():
bigfoot.http.mock_response("GET", "https://api.example.com/a", json={"name": "a"})
bigfoot.http.mock_response("GET", "https://api.example.com/b", json={"name": "b"})

async with bigfoot.sandbox():
async with bigfoot:
a, b = await asyncio.gather(
asyncio.create_task(fetch("https://api.example.com/a")),
asyncio.create_task(fetch("https://api.example.com/b")),
Expand Down Expand Up @@ -78,7 +80,7 @@ async def fetch_in_thread(url: str) -> bytes:
async def test_thread_pool_interception():
bigfoot.http.mock_response("GET", "https://api.example.com/data", body=b"hello")

async with bigfoot.sandbox():
async with bigfoot:
data = await fetch_in_thread("https://api.example.com/data")
assert data == b"hello"

Expand All @@ -98,7 +100,7 @@ async def test_async_mock():
repo = bigfoot.mock("UserRepository")
repo.find_by_id.returns({"id": 1, "name": "Alice"})

async with bigfoot.sandbox():
async with bigfoot:
user = repo.find_by_id(1)
assert user["name"] == "Alice"

Expand Down
12 changes: 6 additions & 6 deletions docs/guides/http-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import bigfoot
def test_api():
bigfoot.http.mock_response("GET", "https://api.example.com/users", json={"users": []})

with bigfoot.sandbox():
with bigfoot:
import httpx
response = httpx.get("https://api.example.com/users")

Expand Down Expand Up @@ -101,7 +101,7 @@ import bigfoot, httpx
def test_users():
bigfoot.http.mock_response("GET", "https://api.example.com/users", json=[])

with bigfoot.sandbox():
with bigfoot:
response = httpx.get("https://api.example.com/users")

bigfoot.assert_interaction(bigfoot.http.request, method="GET", url="https://api.example.com/users",
Expand All @@ -128,7 +128,7 @@ import bigfoot, httpx
def test_httpx_sync():
bigfoot.http.mock_response("GET", "https://api.example.com/data", json={"value": 42})

with bigfoot.sandbox():
with bigfoot:
response = httpx.get("https://api.example.com/data")
assert response.status_code == 200
assert response.json() == {"value": 42}
Expand All @@ -145,7 +145,7 @@ import bigfoot, httpx
async def test_httpx_async():
bigfoot.http.mock_response("POST", "https://api.example.com/items", json={"id": 1}, status=201)

async with bigfoot.sandbox():
async with bigfoot:
async with httpx.AsyncClient() as client:
response = await client.post("https://api.example.com/items", json={"name": "widget"})
assert response.status_code == 201
Expand All @@ -162,7 +162,7 @@ import bigfoot, requests
def test_requests():
bigfoot.http.mock_response("DELETE", "https://api.example.com/items/99", status=204)

with bigfoot.sandbox():
with bigfoot:
response = requests.delete("https://api.example.com/items/99")
assert response.status_code == 204

Expand Down Expand Up @@ -207,7 +207,7 @@ def test_mixed():
bigfoot.http.mock_response("GET", "https://api.example.com/cached", json={"data": "cached"})
bigfoot.http.pass_through("GET", "https://api.example.com/live")

with bigfoot.sandbox():
with bigfoot:
mocked = httpx.get("https://api.example.com/cached") # returns mock response
real = httpx.get("https://api.example.com/live") # makes real HTTP call

Expand Down
8 changes: 4 additions & 4 deletions docs/guides/mock-plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ email.next_id.returns(1)
email.next_id.returns(2)
email.next_id.returns(3)

with bigfoot.sandbox():
with bigfoot:
assert email.next_id() == 1
assert email.next_id() == 2
assert email.next_id() == 3
Expand Down Expand Up @@ -113,7 +113,7 @@ real_service = PaymentService()
payment = bigfoot.spy("PaymentService", real_service)
payment.charge.returns({"id": "mock-123"}) # queue entry: takes priority

with bigfoot.sandbox():
with bigfoot:
result1 = payment.charge(100) # uses queue entry {"id": "mock-123"}
result2 = payment.charge(200) # queue empty: delegates to real_service.charge(200)

Expand Down Expand Up @@ -148,7 +148,7 @@ def test_email():
email = bigfoot.mock("EmailService")
email.send.returns(True)

with bigfoot.sandbox():
with bigfoot:
email.send(to="user@example.com", subject="Welcome")

bigfoot.assert_interaction(email.send, args=(), kwargs={"to": "user@example.com", "subject": "Welcome"})
Expand All @@ -167,7 +167,7 @@ def test_notifications():
email = bigfoot.mock("EmailService")
email.send.returns(True).returns(True)

with bigfoot.sandbox():
with bigfoot:
email.send(to="alice@example.com")
email.send(to="bob@example.com")

Expand Down
12 changes: 7 additions & 5 deletions docs/guides/pytest-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,20 @@ def test_example():
email = bigfoot.mock("EmailService")
email.send.returns(True)

with bigfoot.sandbox():
with bigfoot:
email.send(to="user@example.com")

bigfoot.assert_interaction(email.send)
# verify_all() is called automatically at teardown
```

`with bigfoot:` is shorthand for `with bigfoot.sandbox():`. Both return the active `StrictVerifier` from `__enter__`, so `with bigfoot as v:` gives you the verifier directly if you need it. `bigfoot.sandbox()` remains available as the explicit form for cases where you need to pass the context manager around.

Behind the scenes, an autouse fixture creates one `StrictVerifier` per test, stores it in a `ContextVar`, and calls `verify_all()` after the test completes.

## Async tests

`bigfoot.sandbox()` and `bigfoot.in_any_order()` both support `async with`. Use `pytest-asyncio` for async test functions:
`bigfoot` and `bigfoot.in_any_order()` both support `async with`. Use `pytest-asyncio` for async test functions:

```python
import bigfoot
Expand All @@ -33,7 +35,7 @@ import httpx
async def test_async_http():
bigfoot.http.mock_response("GET", "https://api.example.com/items", json={"items": []})

async with bigfoot.sandbox():
async with bigfoot:
async with httpx.AsyncClient() as client:
response = await client.get("https://api.example.com/items")
assert response.json() == {"items": []}
Expand All @@ -54,7 +56,7 @@ def test_api_call():
bigfoot.http.mock_response("POST", "https://api.example.com/users",
json={"id": 42}, status=201)

with bigfoot.sandbox():
with bigfoot:
response = requests.post("https://api.example.com/users", json={"name": "Alice"})
assert response.status_code == 201
assert response.json()["id"] == 42
Expand Down Expand Up @@ -99,7 +101,7 @@ def test_mixed(bigfoot_verifier: StrictVerifier):
email = bigfoot.mock("EmailService") # same verifier
email.send.returns(True)

with bigfoot.sandbox():
with bigfoot:
email.send(to="user@example.com")

assert bigfoot.current_verifier() is bigfoot_verifier # True
Expand Down
27 changes: 24 additions & 3 deletions docs/guides/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,33 @@ Attribute access on a `MockProxy` returns a `MethodProxy`. `.returns(True)` appe
## Step 4: Enter the sandbox

```python
with bigfoot.sandbox():
with bigfoot:
result = email.send(to="user@example.com", subject="Welcome")
assert result is True
```

`bigfoot.sandbox()` activates all plugins for the current test. Any mock call is intercepted, recorded to the timeline, and dispatched to the configured side effect. Outside the sandbox, calling the mock raises `SandboxNotActiveError`.
`with bigfoot:` is the preferred sandbox syntax. It is shorthand for `with bigfoot.sandbox():`. Both forms activate all plugins for the current test. Any mock call is intercepted, recorded to the timeline, and dispatched to the configured side effect. Outside the sandbox, calling the mock raises `SandboxNotActiveError`.

`with bigfoot:` returns the active `StrictVerifier` from `__enter__`, so you can capture it if needed:

```python
with bigfoot as v:
result = email.send(to="user@example.com", subject="Welcome")
# v is the StrictVerifier for this test
```

This is equivalent to `with bigfoot.sandbox() as v:`. Most tests use the module-level API (`bigfoot.mock()`, `bigfoot.assert_interaction()`, etc.) and never need `v` directly. The main case where you need it is registering custom plugins manually:

```python
import bigfoot
from myapp.plugins import DatabasePlugin

def test_with_custom_plugin():
with bigfoot as v:
db = DatabasePlugin(v) # register plugin on this verifier
db.mock_query("SELECT 1", result=[1])
...
```

## Step 5: Assert interactions

Expand Down Expand Up @@ -123,7 +144,7 @@ def test_welcome_email():
email = bigfoot.mock("EmailService")
email.send.returns(True)

with bigfoot.sandbox():
with bigfoot:
result = email.send(to="user@example.com", subject="Welcome")
assert result is True

Expand Down
Loading