Ape's pytest integration: ape test, built-in fixtures, and per-test isolation#

← Back to Real pytest

ape test: pytest, with Ape already wired in#

ape test is not a separate testing tool from pytest; it is pytest, run through Ape's own command, with Ape's pytest plugin already registered so the fixtures below just exist without you writing a conftest.py for them yourself. Everything from notes/01-pytest-fundamentals.md through notes/03-parametrize.md, test discovery by filename and function name, plain assert, @pytest.fixture, @pytest.mark.parametrize, works exactly the same way under ape test as it does under a bare pytest. Ape's plugin also compiles your contracts first, the same way ape run does, so a stale or uncompiled contract gets caught before your tests even start, rather than failing confusingly partway through one.

The fixtures Ape adds for you#

Two of these, accounts and project, are the ones you'll use directly in almost every test:

  • accounts is a list of accounts that already exist, and already hold funds, on Ape's local test chain. accounts[0], accounts[1], and so on give you distinct addresses to act as different callers, an owner, a non-owner, anyone your test needs. These are not the encrypted keyfile account you imported in Week 5 (accounts.load("testnet-deployer")) and not a real address on Sepolia or any other real testnet; they're throwaway accounts that exist only for the duration of one local test run, pre-funded so you never have to think about gas or faucets while testing.
  • project gives you access to your compiled contracts by name, project.AssetRegistry, project.Counter, whatever you have in your contracts/ folder, the exact same object Week 5's deploy.py used to deploy for real. Calling owner.deploy(project.AssetRegistry) inside a test deploys a fresh instance onto the local test chain, not onto any real network.
  • chain, the third fixture, is a more direct handle onto that same local chain: advancing it by a block (chain.mine()), reading or controlling its notion of the current time, and a few other lower-level operations. Most tests, including everything this module's project step asks for, won't need it directly; it's there for the cases where you need to simulate something about the passage of time or block progression that accounts and project alone don't cover.

Snapshot and revert: why state doesn't leak between tests#

This is the part that makes testing a stateful contract, one that remembers things between calls, genuinely different from testing a plain Python function. Recall notes/02-fixtures.md's point about function-scoped fixtures: by default, pytest reruns a fixture fresh for every test, so one test's changes to a Wallet object never show up in another test's Wallet. Ape's plugin gives you the equivalent guarantee for the entire chain, automatically, with no fixture of your own required: before each test runs, Ape takes a snapshot of the local chain's current state, and the instant that test finishes (pass or fail), Ape reverts the chain back to exactly that snapshot.

That's what the two-test example above is demonstrating. test_increment_updates_value deploys a Counter and increments it to 1. If that change persisted, test_value_starts_at_zero_again would see a counter already at 1 (or, since it deploys its own fresh instance, at least a chain that remembers the first test's transaction happened). It doesn't, because Ape already reverted the chain back to its pre-test snapshot before the second test's owner.deploy(...) ever runs. Neither test has to manually reset anything; the isolation is automatic, and it's the default, not something you opt into.

Why this specifically matters for something like AssetRegistry#

A contract like AssetRegistry exists entirely to remember things: who's registered, what their balance is, whether they're eligible. A test suite for it is going to include tests that register holders, update balances, and flip eligibility flags, exactly the kind of state changes that would bleed into later tests if nothing reset them. Without per-test isolation, the order tests happen to run in would start to matter (a test asserting "this address is not yet registered" could start failing, for no reason related to the code it's actually testing, purely because an earlier test in the same run had already registered that exact address). Ape's snapshot-and-revert isolation is what makes it safe to write each test in project/README.md as if it were the only test in the world: deploy a fresh AssetRegistry, do one thing to it, assert on the result, and trust that nothing from any other test is sitting there already.

What to watch for#

  • Confusing Ape's local test chain with your real testnet deployment. ape test never touches Sepolia (or whichever testnet you used in Week 5), never costs real testnet gas, and needs no RPC URL or private key at all. The AssetRegistry address you've been passing to interact.py is untouched by anything in this module.
  • Assuming accounts[0] is the same account as your imported testnet-deployer. It isn't; it's a separate, local-only test account that Ape funds for you automatically, unrelated to any keyfile you've imported.
  • Writing a test that depends on a previous test having already run (registering a holder in one test, then reading it back in a different test). The isolation this note describes makes that fail, on purpose; each test needs to set up everything it depends on itself, usually via a fixture, as project/README.md shows.

TL;DR#

This page covers Ape's own pytest plugin: the ape test command, the accounts/project/chain fixtures it registers automatically, and the snapshot-and-revert isolation it wraps around every single test.

  • ape test runs pytest, with Ape's plugin already wired in. You don't install or configure anything extra to get accounts, project, and chain as fixtures; they're simply available the moment Ape is installed.
  • accounts gives you a list of already-funded local test accounts, on Ape's own local, disposable test chain, not a real testnet. project gives you your compiled contracts, ready to deploy from within a test.
  • chain is a lower-level handle onto that same local test chain, for the rarer cases where accounts and project alone aren't enough (mining a block, controlling time).
  • Before every single test, Ape snapshots the chain's current state; the moment the test finishes, Ape reverts back to that snapshot. Nothing one test does (deploying a contract, sending a transaction, changing state) is visible to any other test.
  • That isolation is exactly what makes testing a stateful contract sane: every test can assume a clean chain, no matter what order tests run in, the same guarantee notes/02-fixtures.md's function-scoped fixture gives a plain Python object, just applied to an entire blockchain instead of a single variable.
def test_increment_updates_value(project, accounts):
    owner = accounts[0]                       # accounts: funded local test accounts, injected automatically
    counter = owner.deploy(project.Counter)    # project: your compiled contracts, also injected automatically

    counter.increment(sender=owner)

    assert counter.value() == 1


def test_value_starts_at_zero_again(project, accounts):
    owner = accounts[0]
    counter = owner.deploy(project.Counter)

    assert counter.value() == 0   # the increment above never happened here -- Ape reverted the chain back
                                   # to a clean snapshot the moment the previous test finished

# chain (also injected automatically) gives lower-level control over that same local
# test blockchain, like chain.mine() or chain.pending_timestamp, for the rarer cases
# where accounts and project alone aren't enough