Skip to main content

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

  1. How it works
  2. Quick start — PyPI install
  3. Quick start — clone the monorepo demo
  4. Writing tests with @super_seed
  5. superseed.yaml reference
  6. *.schema.yaml reference
  7. Cleanup model (testRunId)
  8. CLI
  9. Full walkthrough repo
  10. 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)
  1. You decorate a test with @super_seed("scenario_name").
  2. The pytest plugin finds superseed.yaml (searches upward from the test file).
  3. It compiles the named scenario to parameterized Cypher statements.
  4. Before the test body runs, it seeds Neo4j and tags created nodes with a unique testRunId.
  5. 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 in MERGE (n:Label {key...})
  • props — extra properties merged after defaults and schema defaults
  • ${parameter} — lowercase placeholders resolved at compile time from parameters + 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 DELETE removes 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


Download files

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

Source Distribution

superseed-0.2.0.tar.gz (14.2 kB view details)

Uploaded Source

Built Distribution

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

superseed-0.2.0-py3-none-any.whl (19.3 kB view details)

Uploaded Python 3

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

Hashes for superseed-0.2.0.tar.gz
Algorithm Hash digest
SHA256 6e444d888f227c8424130c4032ed40c725b245b432b959b30d3f3c5c63687f7f
MD5 c782bce46830f0fde4f87e0b49c3e7a2
BLAKE2b-256 1a4a681fd7e73ff147ab24dc059e32e17c2dd72044a969c841b17ef8c9675b5b

See more details on using hashes here.

Provenance

The following attestation bundles were made for superseed-0.2.0.tar.gz:

Publisher: publish.yml on angelmota/superseed

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

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

Hashes for superseed-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b556f394bc01e86a22589b2ece9ecdbaa7c95d1e0ee1623d60fe12141b1ae91b
MD5 b38e2a87c08bcae73be18bb4bbe2f560
BLAKE2b-256 de348fffa0bed7c27b3fb63b99f1abfdcd0829377bb2a3fc438538f52d643580

See more details on using hashes here.

Provenance

The following attestation bundles were made for superseed-0.2.0-py3-none-any.whl:

Publisher: publish.yml on angelmota/superseed

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page