Skip to content

Commit 3d52c64

Browse files
Use pytest fixtures for integration test teardown
Replace inline create_all/drop_all with a yielding engine fixture to ensure tables are always cleaned up, even when tests fail mid-execution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 939ce97 commit 3d52c64

1 file changed

Lines changed: 32 additions & 91 deletions

File tree

Lines changed: 32 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Integration tests for ORM operations."""
22

33
import datetime
4+
from collections.abc import Generator
45

56
import pytest
67
from sqlalchemy import (
@@ -16,6 +17,7 @@
1617
create_engine,
1718
text,
1819
)
20+
from sqlalchemy.engine import Engine
1921
from sqlalchemy.orm import Session, declarative_base
2022

2123
Base = declarative_base()
@@ -63,6 +65,14 @@ class DateTimeTest(Base): # type: ignore[valid-type,misc]
6365

6466
@pytest.mark.integration
6567
class TestORMOperations:
68+
@pytest.fixture
69+
def engine(self, engine_url: str) -> Generator[Engine]:
70+
engine = create_engine(engine_url)
71+
Base.metadata.create_all(engine)
72+
yield engine
73+
Base.metadata.drop_all(engine)
74+
engine.dispose()
75+
6676
def test_create_engine(self, engine_url: str) -> None:
6777
engine = create_engine(engine_url)
6878
assert engine is not None
@@ -79,108 +89,65 @@ def test_raw_sql(self, engine_url: str) -> None:
7989

8090
engine.dispose()
8191

82-
def test_create_table_and_insert(self, engine_url: str) -> None:
83-
engine = create_engine(engine_url)
84-
85-
# Create tables
86-
Base.metadata.create_all(engine)
87-
88-
# Insert data
92+
def test_create_table_and_insert(self, engine: Engine) -> None:
8993
with Session(engine) as session:
9094
user = User(name="Alice", email="alice@example.com")
9195
session.add(user)
9296
session.commit()
9397

94-
# Query data
9598
users = session.query(User).filter_by(name="Alice").all()
9699
assert len(users) == 1
97100
assert users[0].email == "alice@example.com"
98101

99-
# Cleanup
100-
Base.metadata.drop_all(engine)
101-
engine.dispose()
102-
103-
def test_unicode_text(self, engine_url: str) -> None:
102+
def test_unicode_text(self, engine: Engine) -> None:
104103
"""Test Unicode text handling including emojis, CJK, RTL."""
105-
engine = create_engine(engine_url)
106-
Base.metadata.create_all(engine)
107-
108104
unicode_values = [
109-
# Emojis (4-byte UTF-8)
110-
"Hello 🎉 World",
111-
"🎉🎊🎁🎂",
112-
# CJK characters
113-
"中文测试",
114-
"日本語テスト",
115-
"한국어 테스트",
116-
# RTL languages
117-
"العربية",
118-
"עברית",
119-
# Mixed scripts
120-
"Hello 世界 🌍",
121-
# Combining characters
122-
"café résumé naïve",
105+
"Hello \U0001f389 World",
106+
"\U0001f389\U0001f38a\U0001f381\U0001f382",
107+
"\u4e2d\u6587\u6d4b\u8bd5",
108+
"\u65e5\u672c\u8a9e\u30c6\u30b9\u30c8",
109+
"\ud55c\uad6d\uc5b4 \ud14c\uc2a4\ud2b8",
110+
"\u0627\u0644\u0639\u0631\u0628\u064a\u0629",
111+
"\u05e2\u05d1\u05e8\u05d9\u05ea",
112+
"Hello \u4e16\u754c \U0001f30d",
113+
"caf\u00e9 r\u00e9sum\u00e9 na\u00efve",
123114
]
124115

125116
with Session(engine) as session:
126117
for val in unicode_values:
127-
# Insert
128118
record = UnicodeTest(content=val)
129119
session.add(record)
130120
session.commit()
131121

132-
# Query and verify
133122
result = session.query(UnicodeTest).filter_by(content=val).first()
134123
assert result is not None, f"Failed to find: {repr(val)}"
135124
assert result.content == val, f"Mismatch for: {repr(val)}"
136125

137-
# Cleanup
138126
session.delete(result)
139127
session.commit()
140128

141-
Base.metadata.drop_all(engine)
142-
engine.dispose()
143-
144-
def test_binary_blob(self, engine_url: str) -> None:
129+
def test_binary_blob(self, engine: Engine) -> None:
145130
"""Test binary blob handling including null bytes."""
146-
engine = create_engine(engine_url)
147-
Base.metadata.create_all(engine)
148-
149131
blob_values = [
150132
b"simple",
151-
b"\x00\x01\x02\x03", # Null bytes
152-
b"\xff\xfe\xfd", # High bytes
153-
bytes(range(256)), # All byte values
133+
b"\x00\x01\x02\x03",
134+
b"\xff\xfe\xfd",
135+
bytes(range(256)),
154136
]
155137

156138
with Session(engine) as session:
157139
for val in blob_values:
158-
# Insert
159140
record = BlobTest(data=val)
160141
session.add(record)
161142
session.commit()
162143

163-
# Query and verify
164144
result = session.query(BlobTest).order_by(BlobTest.id.desc()).first()
165145
assert result is not None
166146
assert result.data == val, f"Mismatch for blob: {repr(val)}"
167147

168-
Base.metadata.drop_all(engine)
169-
engine.dispose()
170-
171-
def test_numeric_types(self, engine_url: str) -> None:
172-
"""Test integer, bigint, float, and boolean types.
173-
174-
Note: dqlite has a known limitation where BOOLEAN NULL values are
175-
returned as False (type BOOLEAN with value 0) instead of NULL.
176-
This is because dqlite returns the column's declared type even for
177-
NULL values, and 0 is indistinguishable from NULL for BOOLEAN columns.
178-
"""
179-
engine = create_engine(engine_url)
180-
Base.metadata.create_all(engine)
181-
148+
def test_numeric_types(self, engine: Engine) -> None:
149+
"""Test integer, bigint, float, and boolean types."""
182150
test_cases = [
183-
# (int, bigint, float, bool)
184151
(0, 0, 0.0, False),
185152
(1, 1, 1.0, True),
186153
(-1, -1, -1.0, False),
@@ -199,7 +166,6 @@ def test_numeric_types(self, engine_url: str) -> None:
199166
session.add(record)
200167
session.commit()
201168

202-
# Query and verify
203169
result = session.query(NumericTest).order_by(NumericTest.id.desc()).first()
204170
assert result is not None
205171
assert result.int_val == int_val
@@ -208,56 +174,34 @@ def test_numeric_types(self, engine_url: str) -> None:
208174
assert abs(result.float_val - float_val) < 1e-9
209175
assert result.bool_val == bool_val
210176

211-
Base.metadata.drop_all(engine)
212-
engine.dispose()
213-
214-
def test_datetime_types(self, engine_url: str) -> None:
215-
"""Test DateTime column type.
216-
217-
Note: dqlite has a known limitation where DATETIME NULL values are
218-
returned as empty string instead of NULL. This causes SQLAlchemy's
219-
DateTime processor to fail when parsing. Avoid using NULL datetime
220-
values with dqlite - use a sentinel value if needed.
221-
"""
222-
engine = create_engine(engine_url)
223-
Base.metadata.create_all(engine)
224-
177+
def test_datetime_types(self, engine: Engine) -> None:
178+
"""Test DateTime column type."""
225179
test_dates = [
226180
datetime.datetime(2024, 1, 15, 10, 30, 45),
227-
datetime.datetime(1970, 1, 1, 0, 0, 0), # Unix epoch
228-
datetime.datetime(2038, 1, 19, 3, 14, 7), # Near Y2038
181+
datetime.datetime(1970, 1, 1, 0, 0, 0),
182+
datetime.datetime(2038, 1, 19, 3, 14, 7),
229183
datetime.datetime.now().replace(microsecond=0),
230184
]
231185

232186
with Session(engine) as session:
233187
for dt in test_dates:
234-
# Note: We set updated_at to a value, not NULL, due to dqlite limitation
235188
record = DateTimeTest(created_at=dt, updated_at=dt)
236189
session.add(record)
237190
session.commit()
238191

239-
# Query and verify
240192
result = session.query(DateTimeTest).order_by(DateTimeTest.id.desc()).first()
241193
assert result is not None
242194

243-
# SQLite stores datetime as text, compare with second precision
244195
assert result.created_at.year == dt.year
245196
assert result.created_at.month == dt.month
246197
assert result.created_at.day == dt.day
247198
assert result.created_at.hour == dt.hour
248199
assert result.created_at.minute == dt.minute
249200
assert result.created_at.second == dt.second
250201

251-
Base.metadata.drop_all(engine)
252-
engine.dispose()
253-
254-
def test_null_handling(self, engine_url: str) -> None:
202+
def test_null_handling(self, engine: Engine) -> None:
255203
"""Test NULL values across different column types."""
256-
engine = create_engine(engine_url)
257-
Base.metadata.create_all(engine)
258-
259204
with Session(engine) as session:
260-
# Insert record with all nullable fields as NULL
261205
record = NumericTest(
262206
int_val=None,
263207
bigint_val=None,
@@ -273,6 +217,3 @@ def test_null_handling(self, engine_url: str) -> None:
273217
assert result.bigint_val is None
274218
assert result.float_val is None
275219
assert result.bool_val is None
276-
277-
Base.metadata.drop_all(engine)
278-
engine.dispose()

0 commit comments

Comments
 (0)