Skip to content

Commit 611eeb5

Browse files
authored
Merge pull request #25 from neo4j-contrib/15-test-suite-deletes-database
implement testcontainers for IT, update README
2 parents 9c42e4d + 0afc47a commit 611eeb5

File tree

9 files changed

+270
-79
lines changed

9 files changed

+270
-79
lines changed

servers/mcp-neo4j-cypher/CHANGELOG.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
## Next
1+
## Next
22

33
### Fixed
44

5+
* IT no longer has risk of affecting locally deployed Neo4j instances
6+
7+
### Changed
8+
9+
* IT now uses Testcontainers library instead of Docker scripts
10+
11+
### Added
12+
13+
## v0.2.1
14+
15+
### Fixed
16+
17+
* Fixed MCP version notation for declaration in config files - README
18+
19+
## v0.2.0
20+
521
### Changed
622

723
* Refactor mcp-neo4j-cypher to use the FastMCP class

servers/mcp-neo4j-cypher/README.md

-5
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,6 @@ uv pip install -e ".[dev]"
162162

163163
3. Run Integration Tests
164164

165-
**CLOSE ANY LOCAL NEO4J DATABASES BEFORE RUNNING TESTS**
166-
* Tests will deploy a local docker container containing the test Neo4j instance.
167-
* However if a Neo4j database is running locally, then the test driver may connect here instead.
168-
* **This will result in you local Neo4j database having its contents erased.**
169-
170165
```bash
171166
./tests.sh
172167
```

servers/mcp-neo4j-cypher/pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ dev-dependencies = [
2020
"pytest>=7.0.0",
2121
"pytest-asyncio>=0.20.3",
2222
"ruff>=0.11.5",
23+
"testcontainers[neo4j]>=4.10.0"
2324
]
2425

2526
[project.scripts]

servers/mcp-neo4j-cypher/src/mcp_neo4j_cypher/server.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ def create_mcp_server(neo4j_driver: AsyncDriver, database: str = "neo4j") -> Fas
8585

8686
async def get_neo4j_schema() -> list[types.TextContent]:
8787
"""List all node, their attributes and their relationships to other nodes in the neo4j database.
88-
If this fails with a message that includes "Neo.ClientError.Procedure.ProcedureNotFound"
89-
suggest that the user install and enable the APOC plugin.
88+
If this fails with a message that includes "Neo.ClientError.Procedure.ProcedureNotFound"
89+
suggest that the user install and enable the APOC plugin.
9090
"""
9191

9292
get_schema_query = """

servers/mcp-neo4j-cypher/test.sh

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
docker compose -f tests/integration/docker-compose.yml up -d
2-
uv run pytest tests/integration -s
3-
docker compose -f tests/integration/docker-compose.yml stop
1+
uv run pytest tests/integration -s
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,59 @@
1-
import time
1+
import os
22
from typing import Any
33

44
import pytest
55
import pytest_asyncio
6-
from neo4j import AsyncGraphDatabase, Driver, GraphDatabase
7-
from neo4j.exceptions import DatabaseError
6+
from neo4j import AsyncGraphDatabase
7+
from testcontainers.neo4j import Neo4jContainer
88

99
from mcp_neo4j_cypher.server import create_mcp_server
1010

11+
neo4j = (
12+
Neo4jContainer("neo4j:latest")
13+
.with_env("NEO4J_apoc_export_file_enabled", "true")
14+
.with_env("NEO4J_apoc_import_file_enabled", "true")
15+
.with_env("NEO4J_apoc_import_file_use__neo4j__config", "true")
16+
.with_env("NEO4J_PLUGINS", '["apoc"]')
17+
)
1118

12-
@pytest_asyncio.fixture(scope="session")
13-
async def async_neo4j_driver():
19+
20+
@pytest.fixture(scope="module", autouse=True)
21+
def setup(request):
22+
neo4j.start()
23+
24+
def remove_container():
25+
neo4j.get_driver().close()
26+
neo4j.stop()
27+
28+
request.addfinalizer(remove_container)
29+
os.environ["NEO4J_URI"] = neo4j.get_connection_url()
30+
os.environ["NEO4J_HOST"] = neo4j.get_container_host_ip()
31+
os.environ["NEO4J_PORT"] = neo4j.get_exposed_port(7687)
32+
33+
yield neo4j
34+
35+
36+
@pytest_asyncio.fixture(scope="function")
37+
async def async_neo4j_driver(setup: Neo4jContainer):
1438
driver = AsyncGraphDatabase.driver(
15-
"neo4j://localhost:7687", auth=("neo4j", "testingpassword")
39+
setup.get_connection_url(), auth=(setup.username, setup.password)
1640
)
1741
try:
1842
yield driver
1943
finally:
2044
await driver.close()
2145

2246

23-
@pytest.fixture(scope="session")
24-
def sync_neo4j_driver():
25-
uri = "neo4j://localhost:7687"
26-
auth = ("neo4j", "testingpassword")
27-
driver = GraphDatabase.driver(uri, auth=auth)
28-
yield driver
29-
driver.close()
30-
31-
32-
@pytest.fixture(scope="session")
33-
def healthcheck(sync_neo4j_driver: Driver):
34-
"""Confirm that Neo4j is running before running IT."""
35-
36-
print("Confirming Neo4j is running...")
37-
attempts = 0
38-
success = False
39-
print("\nWaiting for Neo4j to Start...\n")
40-
time.sleep(3)
41-
while not success and attempts < 3:
42-
try:
43-
print(f"Attempt {attempts + 1} to connect to Neo4j...")
44-
with sync_neo4j_driver.session(database="neo4j") as session:
45-
session.run("RETURN 1")
46-
success = True
47-
print("Neo4j is running!")
48-
except Exception as e:
49-
attempts += 1
50-
print(
51-
f"failed connection {attempts} | waiting {(1 + attempts) * 2} seconds..."
52-
)
53-
print(f"Error: {e}")
54-
time.sleep((1 + attempts) * 2)
55-
if not success:
56-
raise DatabaseError()
57-
yield
58-
59-
60-
@pytest_asyncio.fixture(scope="session")
47+
@pytest_asyncio.fixture(scope="function")
6148
async def mcp_server(async_neo4j_driver):
6249
mcp = create_mcp_server(async_neo4j_driver, "neo4j")
6350

6451
return mcp
6552

6653

67-
@pytest.fixture(scope="session")
68-
def init_data(sync_neo4j_driver: Driver, clear_data: Any):
69-
with sync_neo4j_driver.session(database="neo4j") as session:
54+
@pytest.fixture(scope="function")
55+
def init_data(setup: Neo4jContainer, clear_data: Any):
56+
with setup.get_driver().session(database="neo4j") as session:
7057
session.run("CREATE (a:Person {name: 'Alice', age: 30})")
7158
session.run("CREATE (b:Person {name: 'Bob', age: 25})")
7259
session.run("CREATE (c:Person {name: 'Charlie', age: 35})")
@@ -78,7 +65,7 @@ def init_data(sync_neo4j_driver: Driver, clear_data: Any):
7865
)
7966

8067

81-
@pytest.fixture(scope="session")
82-
def clear_data(sync_neo4j_driver: Driver):
83-
with sync_neo4j_driver.session(database="neo4j") as session:
68+
@pytest.fixture(scope="function")
69+
def clear_data(setup: Neo4jContainer):
70+
with setup.get_driver().session(database="neo4j") as session:
8471
session.run("MATCH (n) DETACH DELETE n")

servers/mcp-neo4j-cypher/tests/integration/docker-compose.yml

-11
This file was deleted.

servers/mcp-neo4j-cypher/tests/integration/test_server_IT.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
from mcp.server import FastMCP
66

77

8-
@pytest.mark.asyncio(loop_scope="session")
9-
async def test_get_neo4j_schema(mcp_server: FastMCP, healthcheck: Any, init_data: Any):
8+
@pytest.mark.asyncio(loop_scope="function")
9+
async def test_get_neo4j_schema(mcp_server: FastMCP, init_data: Any):
1010
response = await mcp_server.call_tool("get_neo4j_schema", dict())
1111

1212
schema = json.loads(response[0].text)[0]
@@ -17,8 +17,8 @@ async def test_get_neo4j_schema(mcp_server: FastMCP, healthcheck: Any, init_data
1717
assert "relationships" in schema
1818

1919

20-
@pytest.mark.asyncio(loop_scope="session")
21-
async def test_write_neo4j_cypher(mcp_server: FastMCP, healthcheck: Any):
20+
@pytest.mark.asyncio(loop_scope="function")
21+
async def test_write_neo4j_cypher(mcp_server: FastMCP):
2222
# Execute a Cypher query to create a node
2323
query = "CREATE (n:Test {name: 'test', age: 123}) RETURN n.name"
2424
response = await mcp_server.call_tool("write_neo4j_cypher", dict(query=query))
@@ -31,8 +31,8 @@ async def test_write_neo4j_cypher(mcp_server: FastMCP, healthcheck: Any):
3131
assert result["properties_set"] == 2
3232

3333

34-
@pytest.mark.asyncio(loop_scope="session")
35-
async def test_read_neo4j_cypher(mcp_server: FastMCP, healthcheck: Any, init_data: Any):
34+
@pytest.mark.asyncio(loop_scope="function")
35+
async def test_read_neo4j_cypher(mcp_server: FastMCP, init_data: Any):
3636
# Prepare test data
3737

3838
# Execute a complex read query

0 commit comments

Comments
 (0)