Declarative Neo4j fixtures for pytest
Project description
SuperSeed
Declarative Neo4j fixtures for pytest. Seed real graph data before each test, run your services against it, then tear it down automatically.
This is not mocking. SuperSeed does not patch repositories or stub the driver. It loads YAML scenarios, compiles them to Cypher,
MERGEs nodes and relationships into Neo4j, and deletes everything tagged with a per-run ID when the test finishes.
Requirements: Python 3.11+, Neo4j 5+, pytest
Table of contents
- How it works
- Quick start — PyPI install
- Quick start — clone the monorepo demo
- Writing tests with
@super_seed superseed.yamlreference*.schema.yamlreference- Cleanup model (
testRunId) - CLI
- Full walkthrough repo
- Contributing
How it works
sequenceDiagram
autonumber
participant Dev as Your test
participant Plugin as SuperSeed plugin
participant YAML as superseed.yaml
participant Compiler as compiler
participant Neo4j as Neo4j
participant App as Service / repository
Dev->>Plugin: @super_seed("matrix_cast")
Plugin->>YAML: find config + load scenario
Plugin->>Compiler: compile_scenario(run_id)
Compiler-->>Plugin: CypherSeedPlan (MERGE statements)
Plugin->>Neo4j: seed — MERGE nodes & relationships
Note over Neo4j: every node tagged with testRunId
Plugin->>Dev: run test body
Dev->>App: call business logic
App->>Neo4j: real driver queries
Neo4j-->>App: graph results
App-->>Dev: assertions pass / fail
Plugin->>Neo4j: cleanup(testRunId)
Note over Neo4j: DETACH DELETE seeded nodes
superseed.yaml scenario
│
▼
compile to Cypher (MERGE nodes + relationships)
│
▼
seed Neo4j (tag every node with testRunId)
│
▼
your pytest runs (real service / repository code)
│
▼
cleanup (DELETE all nodes with that testRunId)
- You decorate a test with
@super_seed("scenario_name"). - The pytest plugin finds
superseed.yaml(searches upward from the test file). - It compiles the named scenario to parameterized Cypher statements.
- Before the test body runs, it seeds Neo4j and tags created nodes with a unique
testRunId. - After the test (pass or fail), it deletes every node with that
testRunId.
Your tests talk to real Neo4j through your real application code — the same code you run in production.
Quick start — PyPI install
Use this path when SuperSeed is a dependency in your own project.
1. Install
pip install superseed
# or
uv add superseed
You also need a running Neo4j instance. Minimal Docker setup:
docker run -d \
--name neo4j \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/your-password \
neo4j:5
2. Add superseed.yaml beside your tests
neo4j:
uri: ${NEO4J_URI:-bolt://localhost:7687}
user: neo4j
password: ${NEO4J_PASSWORD:-your-password}
defaults:
Movie:
released: 1999
scenarios:
matrix_cast:
description: "The Matrix with Neo and Trinity"
parameters:
movie_title: "The Matrix"
nodes:
- label: Movie
key: {title: "${movie_title}"}
props: {released: 1999}
- label: Actor
key: {name: "Keanu Reeves"}
- label: Actor
key: {name: "Carrie-Anne Moss"}
props: {born: 1967}
relationships:
- type: ACTED_IN
from: {label: Actor, key: {name: "Keanu Reeves"}}
to: {label: Movie, key: {title: "${movie_title}"}}
props: {role: "Neo"}
- type: ACTED_IN
from: {label: Actor, key: {name: "Carrie-Anne Moss"}}
to: {label: Movie, key: {title: "${movie_title}"}}
props: {role: "Trinity"}
Set environment variables for your Neo4j connection:
export NEO4J_URI=bolt://localhost:7687
export NEO4J_PASSWORD=your-password
3. Write a test
from superseed import super_seed
@super_seed("matrix_cast")
def test_get_cast(movie_service):
cast = movie_service.get_cast_for_movie("The Matrix")
assert len(cast) == 2
The plugin loads automatically when pytest discovers SuperSeed (via the pytest11 entry point). No pytest_plugins line required if superseed is installed.
4. Run
pytest tests/ -v
superseed validate -c tests/superseed.yaml # optional CI check
Quick start — clone the monorepo demo
Use this path to explore the reference app and dogfood the library locally.
git clone <repo-url> superseed && cd superseed
cp .env.example .env
docker compose up --build -d # Neo4j + movie-api
# Package dev setup
uv sync --extra dev
# Run the demo integration tests (Neo4j only — movie-api container optional)
export NEO4J_URI=bolt://localhost:7687
export NEO4J_PASSWORD=superseed-dev
cd examples/movie-api && uv sync --extra dev && uv run pytest -v
| URL | What |
|---|---|
| http://localhost:7474 | Neo4j Browser (neo4j / superseed-dev) |
| http://localhost:8000/docs | Movie API (FastAPI demo) |
bolt://localhost:7687 |
Host pytest / CLI |
After running a seeded test you can inspect the graph in Neo4j Browser or hit the API:
uv run pytest -k matrix -v
curl http://localhost:8000/movies/The%20Matrix/actors
Writing tests with @super_seed
Basic usage
from superseed import super_seed
@super_seed("matrix_cast")
def test_get_cast_for_matrix(movie_service):
cast = movie_service.get_cast_for_movie("The Matrix")
assert len(cast) == 2
Parameter overrides
Override scenario parameters per test without duplicating YAML:
@super_seed("matrix_cast", movie_title="Inception")
def test_custom_title(movie_service):
...
In YAML, reference parameters with lowercase placeholders: ${movie_title}.
Config discovery
By default the plugin searches upward from the test file for superseed.yaml. Override with:
pytest --superseed-config path/to/superseed.yaml
Optional schema validation:
pytest --superseed-schema path/to/movies.schema.yaml
If omitted, the plugin looks for movies.schema.yaml or superseed.schema.yaml next to the config file.
What you should not do
# Before SuperSeed — don't do this anymore
def test_old_way(neo4j_session, movie_service):
neo4j_session.run("CREATE (m:Movie ...)") # hand-written seed
...
neo4j_session.run("MATCH (n) DETACH DELETE n") # manual cleanup
SuperSeed replaces inline CREATE blocks and manual teardown.
superseed.yaml reference
Top-level keys:
| Key | Required | Description |
|---|---|---|
neo4j |
yes | Connection settings (uri, user, password) |
defaults |
no | Default properties per label, merged into every node |
scenarios |
yes | Named scenario definitions |
Environment substitution
Use uppercase env vars in config values:
neo4j:
uri: ${NEO4J_URI:-bolt://localhost:7687}
password: ${NEO4J_PASSWORD:-superseed-dev}
Syntax: ${VAR} or ${VAR:-default}.
Scenario fields
| Field | Required | Description |
|---|---|---|
description |
no | Human-readable summary (shown by superseed list) |
linked_repository |
no | Dotted repo method path for future auto-detection, e.g. MovieRepository.find_actors_for_movie |
parameters |
no | Default parameter values; overridable via @super_seed(..., param=value) |
required_labels |
no | Documentation / scan metadata |
required_relationships |
no | Documentation / scan metadata |
nodes |
yes | Nodes to MERGE |
relationships |
no | Relationships to MERGE between matched nodes |
Node entry
- label: Movie
key: {title: "${movie_title}"} # MERGE identity — must be unique in the graph
props: {released: 1999} # additional SET properties
key— business identity used inMERGE (n:Label {key...})props— extra properties merged after defaults and schema defaults${parameter}— lowercase placeholders resolved at compile time fromparameters+ decorator overrides
Relationship entry
- type: ACTED_IN
from: {label: Actor, key: {name: "Keanu Reeves"}}
to: {label: Movie, key: {title: "${movie_title}"}}
props: {role: "Neo"}
The compiler MATCHes both endpoints by key, then MERGEs the relationship.
*.schema.yaml reference
Optional label contract validated before compile. Place beside superseed.yaml (e.g. movies.schema.yaml).
Movie:
required: [title]
optional: [released, id]
defaults:
released: 1999
Actor:
required: [name]
optional: [born, id]
defaults:
born: 1964
| Key | Description |
|---|---|
required |
Properties that must appear in key + props (after defaults) |
optional |
Allowed extra properties |
defaults |
Applied before scenario props |
Run superseed validate -c tests/superseed.yaml to catch schema violations in CI.
Cleanup model (testRunId)
Every seeded node gets a property:
testRunId = "<uuid per test run>"
Cleanup query (run automatically after each @super_seed test):
MATCH (n {testRunId: $run_id}) DETACH DELETE n
Why this works:
- Each test run gets a fresh UUID — no collisions between parallel or sequential tests.
DETACH DELETEremoves relationships automatically when nodes are deleted.- Tests are isolated: one test's graph never leaks into the next.
- Failed tests still clean up (teardown runs regardless of pass/fail).
Relationships are not tagged; they disappear when their endpoint nodes are deleted.
CLI
superseed validate -c path/to/superseed.yaml # parse YAML + schema; exit 1 on error
superseed list -c path/to/superseed.yaml # print scenario names + descriptions
superseed scan path/to/repositories # suggest scenario stubs from repo Cypher
superseed scan
Walks *repository*.py files, extracts Cypher via AST, and writes:
.superseed/
├── suggested/
│ ├── find_actors_for_movie.yaml # merge into superseed.yaml
│ └── ...
└── repo_index.json # machine-readable index for tooling
Each suggested stub includes linked_repository, detected labels, and relationship types. Edit the stub, copy the scenario into superseed.yaml, and run superseed validate.
Example:
superseed scan app/repositories -o .superseed
superseed validate -c tests/superseed.yaml
Full walkthrough repo
The git monorepo includes a complete reference app that dogfoods SuperSeed:
examples/movie-api/
├── app/
│ ├── repositories/movie_repository.py # real Cypher strings (scan targets these)
│ ├── services/movie_service.py
│ └── main.py # FastAPI demo
└── tests/
├── superseed.yaml # scenarios
├── movies.schema.yaml # label contract
└── test_movie_service.py # @super_seed tests — no inline CREATE
See examples/movie-api/tests/test_movie_service.py and examples/movie-api/tests/superseed.yaml for a working end-to-end example.
Contributing
SuperSeed is developed in the open. Planning docs:
Local development:
uv sync --extra dev
uv run pytest tests/ -v
uv run ruff check src tests
uv run mypy src
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file superseed-0.2.0.tar.gz.
File metadata
- Download URL: superseed-0.2.0.tar.gz
- Upload date:
- Size: 14.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6e444d888f227c8424130c4032ed40c725b245b432b959b30d3f3c5c63687f7f
|
|
| MD5 |
c782bce46830f0fde4f87e0b49c3e7a2
|
|
| BLAKE2b-256 |
1a4a681fd7e73ff147ab24dc059e32e17c2dd72044a969c841b17ef8c9675b5b
|
Provenance
The following attestation bundles were made for superseed-0.2.0.tar.gz:
Publisher:
publish.yml on angelmota/superseed
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
superseed-0.2.0.tar.gz -
Subject digest:
6e444d888f227c8424130c4032ed40c725b245b432b959b30d3f3c5c63687f7f - Sigstore transparency entry: 1786883061
- Sigstore integration time:
-
Permalink:
angelmota/superseed@a983f8ddb882f6900927072d7b37ad6768d1dd72 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/angelmota
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a983f8ddb882f6900927072d7b37ad6768d1dd72 -
Trigger Event:
push
-
Statement type:
File details
Details for the file superseed-0.2.0-py3-none-any.whl.
File metadata
- Download URL: superseed-0.2.0-py3-none-any.whl
- Upload date:
- Size: 19.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b556f394bc01e86a22589b2ece9ecdbaa7c95d1e0ee1623d60fe12141b1ae91b
|
|
| MD5 |
b38e2a87c08bcae73be18bb4bbe2f560
|
|
| BLAKE2b-256 |
de348fffa0bed7c27b3fb63b99f1abfdcd0829377bb2a3fc438538f52d643580
|
Provenance
The following attestation bundles were made for superseed-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on angelmota/superseed
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
superseed-0.2.0-py3-none-any.whl -
Subject digest:
b556f394bc01e86a22589b2ece9ecdbaa7c95d1e0ee1623d60fe12141b1ae91b - Sigstore transparency entry: 1786883095
- Sigstore integration time:
-
Permalink:
angelmota/superseed@a983f8ddb882f6900927072d7b37ad6768d1dd72 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/angelmota
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a983f8ddb882f6900927072d7b37ad6768d1dd72 -
Trigger Event:
push
-
Statement type: