Skip to content

Commit 12195f2

Browse files
committed
test(d1): tests/integration/ + Testcontainers MySQL harness
延续 v0.2.2 末尾"凭据清理"的方向 - 让集成测试可以在零外部依赖 (Docker 拉 MySQL 8.0 容器) 下跑完。 新增: - tests/integration/conftest.py: 提供 mysql_container session fixture * 优先用 DBJAVAGENIX_TEST_DB_HOST (env-var 路径,生产 DB) * 否则用 testcontainers 起 MySQL 8.0 容器 * --no-docker flag 跳过所有依赖 Docker 的测试 * testcontainers / Docker 任一缺失 → pytest.skip() 而不是 fail - tests/integration/test_harness_smoke.py: 2 个 meta-test 验证 fixture 本身 pyproject.toml: 新增 optional-deps `integration` = testcontainers[mysql,postgres] Why: 凭据清理后必须给"如何在 CI 跑集成测试"一个标准答案。Testcontainers 是社区共识 (Java Spring Boot 测试也基本都走它),Python 版 API 成熟。 D1 是 v0.3 多 dialect 工作 (D2-D3 PostgreSQL) 的地基。
1 parent 774260c commit 12195f2

4 files changed

Lines changed: 156 additions & 0 deletions

File tree

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ dev = [
5151
"pre-commit>=4.0.0",
5252
]
5353

54+
integration = [
55+
"testcontainers[mysql,postgres]>=4.8.0",
56+
]
57+
5458
docs = [
5559
"mkdocs>=1.5.0",
5660
"mkdocs-material>=9.0.0",

tests/integration/__init__.py

Whitespace-only changes.

tests/integration/conftest.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""Pytest harness for integration tests against ephemeral DB containers.
2+
3+
Uses `testcontainers` (https://testcontainers-python.readthedocs.io/) to spin
4+
up real MySQL / PostgreSQL via Docker for each test session. This replaces
5+
the previous workflow of pointing at a fixed shared dev DB (which leaked
6+
credentials and required network access from CI).
7+
8+
If `testcontainers` is not installed, or Docker is not available, the
9+
fixtures `skip()` instead of erroring so unit tests can still run.
10+
11+
Usage:
12+
13+
pytest tests/integration/ # auto-spins MySQL container
14+
pytest tests/integration/ --no-docker # skips all integration tests
15+
16+
The legacy env-var path (DBJAVAGENIX_TEST_DB_HOST) is still respected via
17+
`existing_db_config` for runs against a real shared DB.
18+
"""
19+
20+
from __future__ import annotations
21+
22+
import os
23+
import socket
24+
from typing import Optional
25+
26+
import pytest
27+
28+
29+
# ---------------------------------------------------------------------------
30+
# CLI flag: --no-docker disables container-based fixtures entirely
31+
# ---------------------------------------------------------------------------
32+
33+
34+
def pytest_addoption(parser):
35+
parser.addoption(
36+
"--no-docker",
37+
action="store_true",
38+
default=False,
39+
help="Skip integration tests that need Docker / testcontainers.",
40+
)
41+
42+
43+
# ---------------------------------------------------------------------------
44+
# Helpers
45+
# ---------------------------------------------------------------------------
46+
47+
48+
def _docker_available() -> bool:
49+
"""Best-effort check: probe `docker` socket / pipe without subprocess."""
50+
if os.name == "nt":
51+
# Windows: Docker Desktop pipe at \\.\pipe\docker_engine — hard to probe
52+
# without ctypes. Defer to import-time error from testcontainers.
53+
return True
54+
return os.path.exists("/var/run/docker.sock")
55+
56+
57+
def _testcontainers_available() -> bool:
58+
try:
59+
import testcontainers # noqa: F401
60+
return True
61+
except ImportError:
62+
return False
63+
64+
65+
def _port_open(host: str, port: int, timeout: float = 1.0) -> bool:
66+
"""Quick TCP probe — used to verify container is ready."""
67+
try:
68+
with socket.create_connection((host, port), timeout=timeout):
69+
return True
70+
except OSError:
71+
return False
72+
73+
74+
# ---------------------------------------------------------------------------
75+
# Fixtures
76+
# ---------------------------------------------------------------------------
77+
78+
79+
@pytest.fixture(scope="session")
80+
def existing_db_config() -> Optional[dict]:
81+
"""If user exported DBJAVAGENIX_TEST_DB_HOST, use that instead of a container."""
82+
from tests._db_config import get_test_db_config
83+
return get_test_db_config()
84+
85+
86+
@pytest.fixture(scope="session")
87+
def mysql_container(request, existing_db_config):
88+
"""Yields a dict { host, port, user, password, database, database_type }.
89+
90+
Resolution order:
91+
1. If --no-docker flag set: skip
92+
2. If existing_db_config exists (env vars): use it
93+
3. If testcontainers + docker available: spin up MySQL container
94+
4. Otherwise: skip with reason
95+
"""
96+
if request.config.getoption("--no-docker"):
97+
pytest.skip("--no-docker passed")
98+
99+
if existing_db_config and existing_db_config["database_type"] == "mysql":
100+
# Trust env-var DB. Verify reachable before yielding.
101+
if not _port_open(existing_db_config["host"], existing_db_config["port"]):
102+
pytest.skip(
103+
f"DBJAVAGENIX_TEST_DB_HOST={existing_db_config['host']}:"
104+
f"{existing_db_config['port']} not reachable"
105+
)
106+
yield existing_db_config
107+
return
108+
109+
if not _testcontainers_available():
110+
pytest.skip(
111+
"testcontainers not installed (pip install dbjavagenix[integration])"
112+
)
113+
if not _docker_available():
114+
pytest.skip("Docker not available")
115+
116+
from testcontainers.mysql import MySqlContainer # type: ignore
117+
118+
with MySqlContainer("mysql:8.0") as container:
119+
config = {
120+
"host": container.get_container_host_ip(),
121+
"port": int(container.get_exposed_port(3306)),
122+
"username": container.username,
123+
"password": container.password,
124+
"database": container.dbname,
125+
"database_type": "mysql",
126+
}
127+
yield config
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Smoke test: container fixture yields a usable DB config.
2+
3+
This is a meta-test of the harness itself — it doesn't depend on
4+
DBJavaGenix application code. If this passes, downstream integration tests
5+
for discovery / codegen can build on the same fixture.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
11+
def test_mysql_container_yields_config(mysql_container):
12+
"""The fixture returns a dict with all expected keys."""
13+
assert isinstance(mysql_container, dict)
14+
for key in ("host", "port", "username", "password", "database", "database_type"):
15+
assert key in mysql_container, f"missing key {key}"
16+
assert mysql_container["database_type"] == "mysql"
17+
assert isinstance(mysql_container["port"], int)
18+
assert mysql_container["port"] > 0
19+
20+
21+
def test_mysql_container_reachable(mysql_container):
22+
"""We can actually open a TCP connection to the yielded host:port."""
23+
from tests.integration.conftest import _port_open
24+
25+
assert _port_open(mysql_container["host"], mysql_container["port"], timeout=5.0)

0 commit comments

Comments
 (0)