Skip to content

Commit d5d809e

Browse files
committed
🔧 chore: Update test configuration and add integration tests for pgvector
- Modify `pytest.ini` to exclude integration tests by default, requiring explicit invocation. - Replace `pytest-postgresql` with `testcontainers[postgres]` in `test_requirements.txt` for better container management. - Update CI workflow to run unit and integration tests separately, generating distinct test result files. - Introduce shared fixtures and integration tests for pgvector, ensuring proper SQL query performance and index usage in a real PostgreSQL environment.
1 parent bebfd2b commit d5d809e

6 files changed

Lines changed: 949 additions & 6 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,21 @@ jobs:
3232
pip install -r requirements.txt
3333
pip install -r test_requirements.txt
3434
35-
- name: Run tests and generate report
35+
- name: Run unit tests
3636
env:
3737
OPENAI_API_KEY: test_key
3838
run: |
39-
pytest --maxfail=1 --disable-warnings --junitxml=test-results.xml
39+
pytest --maxfail=1 --disable-warnings --junitxml=test-results-unit.xml
40+
41+
- name: Run integration tests (pgvector container)
42+
env:
43+
OPENAI_API_KEY: test_key
44+
run: |
45+
pytest -m integration --maxfail=3 --disable-warnings --junitxml=test-results-integration.xml
4046
4147
- name: Upload test results
48+
if: always()
4249
uses: actions/upload-artifact@v4
4350
with:
4451
name: test-results
45-
path: test-results.xml
52+
path: test-results-*.xml

‎pytest.ini‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ markers =
1313
integration: marks tests as integration tests (may require more resources)
1414
slow: marks tests as slow running
1515

16-
# Default options
17-
addopts = -v --tb=short
16+
# Default options — exclude integration tests (require Docker) unless explicitly requested
17+
addopts = -v --tb=short -m "not integration"
1818

1919
# Filter warnings
2020
filterwarnings =

‎test_requirements.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
pytest==8.3.4
22
pytest-asyncio==0.26.0
3-
pytest-postgresql==7.0.1
3+
testcontainers[postgres]>=4.14.0
44
mongomock==4.3.0
55
httpx==0.27.0

‎tests/integration/__init__.py‎

Whitespace-only changes.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"""Shared fixtures for integration tests that use a real pgvector PostgreSQL container.
2+
3+
Equivalent to mongodb-memory-server in Node.js: spins up a real, ephemeral
4+
PostgreSQL instance with pgvector for production-parity testing.
5+
"""
6+
7+
import pytest
8+
import sqlalchemy
9+
from sqlalchemy import text
10+
from sqlalchemy.orm import Session
11+
from testcontainers.postgres import PostgresContainer
12+
13+
PGVECTOR_IMAGE = "pgvector/pgvector:pg16"
14+
15+
16+
@pytest.fixture(scope="session")
17+
def pg_container():
18+
"""Start a pgvector PostgreSQL container once for the entire test session."""
19+
with PostgresContainer(PGVECTOR_IMAGE, driver="psycopg2") as pg:
20+
yield pg
21+
22+
23+
@pytest.fixture(scope="session")
24+
def pg_url(pg_container):
25+
"""SQLAlchemy connection URL for the test container."""
26+
return pg_container.get_connection_url()
27+
28+
29+
@pytest.fixture(scope="session")
30+
def engine(pg_url):
31+
"""Session-scoped SQLAlchemy engine connected to the test container."""
32+
eng = sqlalchemy.create_engine(pg_url)
33+
with eng.begin() as conn:
34+
conn.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
35+
yield eng
36+
eng.dispose()
37+
38+
39+
@pytest.fixture(scope="session")
40+
def _create_tables(engine):
41+
"""Create the langchain tables and indexes once for the session.
42+
43+
Mirrors the production schema created by LangChain PGVector + our
44+
ensure_vector_indexes() startup logic.
45+
"""
46+
with engine.begin() as conn:
47+
conn.execute(
48+
text(
49+
"""
50+
CREATE TABLE IF NOT EXISTS langchain_pg_collection (
51+
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
52+
name VARCHAR NOT NULL UNIQUE,
53+
cmetadata JSONB
54+
)
55+
"""
56+
)
57+
)
58+
conn.execute(
59+
text(
60+
"""
61+
CREATE TABLE IF NOT EXISTS langchain_pg_embedding (
62+
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
63+
collection_id UUID REFERENCES langchain_pg_collection(uuid) ON DELETE CASCADE,
64+
embedding vector(3),
65+
document VARCHAR,
66+
cmetadata JSONB,
67+
custom_id VARCHAR
68+
)
69+
"""
70+
)
71+
)
72+
conn.execute(
73+
text(
74+
"""
75+
CREATE INDEX IF NOT EXISTS idx_langchain_pg_embedding_file_id
76+
ON langchain_pg_embedding ((cmetadata->>'file_id'))
77+
"""
78+
)
79+
)
80+
conn.execute(
81+
text(
82+
"""
83+
CREATE INDEX IF NOT EXISTS ix_cmetadata_gin
84+
ON langchain_pg_embedding
85+
USING gin (cmetadata jsonb_path_ops)
86+
"""
87+
)
88+
)
89+
90+
91+
@pytest.fixture(scope="session")
92+
def collection_id(engine, _create_tables):
93+
"""Insert a test collection and return its UUID."""
94+
with engine.begin() as conn:
95+
row = conn.execute(
96+
text(
97+
"INSERT INTO langchain_pg_collection (name, cmetadata) "
98+
"VALUES (:name, :meta) RETURNING uuid"
99+
),
100+
{"name": "test_collection", "meta": "{}"},
101+
).fetchone()
102+
return row[0]
103+
104+
105+
@pytest.fixture()
106+
def db_session(engine, _create_tables):
107+
"""Per-test session that rolls back after each test for isolation."""
108+
conn = engine.connect()
109+
trans = conn.begin()
110+
session = Session(bind=conn)
111+
yield session, conn
112+
session.close()
113+
trans.rollback()
114+
conn.close()

0 commit comments

Comments
 (0)