結論
import gc
gc.collect() #←これが効いた
自分の環境では?
@pytest.fixture
def setup_db():
# インスタンスを返す
yield Repo
# 以下は、テスト終了後に行われる処理
gc.collect() #これが無くて ERROR test_case1.py::test_1_test - PermissionError: [WinError 32] プロセスはファイルにアクセスできません。別のプロセスが使用中です。: 'test.db' となっていた
Path("test.db").unlink(missing_ok=True)
全体のコード(動作確認済み)
VSCodeのターミナルで以下を実行
pytest test_case1.py -rA #-rAを使ってprint文の補足
Repoクラスは、テスト対象を模したコード
import sqlite3
import pytest
from pathlib import Path
import gc
class Repo:
def __init__(self) -> None:
with sqlite3.connect("test.db") as conn:
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER
)
""")
conn.commit()
def a1(self):
with sqlite3.connect("test.db") as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO users(name, age) VALUES ('taro','4')
""")
conn.commit()
def show(self):
with sqlite3.connect("test.db") as conn:
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM users
""")
result = cursor.fetchone()
print(result)
@pytest.fixture
def setup_db():
yield Repo
gc.collect()
Path("test.db").unlink(missing_ok=True)
def test_1_test(setup_db):
r = setup_db()
r.show()
r.a1()
r.show()
assert True
経緯
Webアプリ開発におけるレイヤー設計コードの検証をしていて、
Repositoryレイヤーのpytestを書こうとすると
Repositoryレイヤー内のコードがconnectやcursorを保持せずwith文で接続を完結させるように書いていたため、pytestでは、SQLite3.connect(":memory:")を使えなかった。
そこでダミーのtest.dbを作って、fixtureの終了後にtest.dbを消そうとすると「PermissionError: [WinError 32] プロセスはファイルにアクセスできません。別のプロセスが使用中です。: 'test.db'」
となった(※Macでは発生しない)ので、色々やってみて苦戦してAIに聞いてみた結果
最終的には、gc.collect()が効いた
[参考]AIとの会話
AIに聞いてみました。
🤔私
windows環境のpytest で SQLiteでdbファイルを作ってそのdbのテストをしていますが、終了後処理でダミーのdbファイルを消そうとするとプロセスが使用中となります。原因を分析してくれませんか?
🤖AI(Gemma4 E4Bさん)
ご質問ありがとうございます。これは、PytestやPythonのテスト環境において、特にWindows環境でSQLiteデータベースファイルを扱う際非常によく遭遇する「古典的」な問題です。
原因は基本的に 「プロセスが使用中(File Locked)」のエラーメッセージ通り、「ファイルハンドル(File Handle)」が完全に解放されていないことにあります。
単にコード上では処理が完了し、接続を閉じているように見えても、Pythonの内部的な参照やOSカーネルレベルで開いている「ハンドル」が残っているため、デリート処理を実行するプロセスから見て「まだ使われているファイル」と認識されてしまうのです。
原因分析と、具体的な対処法を3つのレイヤー(コード層、テスト構造層、環境対応)に分けてご説明します。
🕵️♂️ 原因分析:なぜファイルが解放されないのか?
1. 最も可能性の高い原因:「接続オブジェクトの参照漏れ」
Pythonでsqlite3を扱う際、開発者はしばしばコネクションを閉じます(例: conn.close())。しかし、以下の状況があると、単に.close()するだけでは不十分な場合があります。
- ローカル変数の参照: テスト実行中に、開いたデータベース接続オブジェクトやカーソル(Cursor)がスコープ外に出る前にどこか別の場所で一時的に保持されてしまっている場合。
-
例外処理の問題: 正常にクリーンアップコードが動く前提で書かれているが、まれな例外が発生した場合、
finallyブロックなどでの適切なクローズ処理がスキップされる場合がある。(ただし、これはコード設計のミスです。)
2. Windows特有の原因:「OSレベルのロック」
Windows OSはファイルアクセスに非常に厳格です。一度プロセス(この場合はPython仮想環境)がファイルを読み書きするハンドルを持った場合、そのシステムコールが完全に完了し、「解放された」と判断されるまで、外部からの強制的な削除(os.remove()など)をブロックすることがあります。
3. Pytest Fixtureの「寿命」の問題
テストファイルやPytestのファクトリ(Fixture)の実行タイミングで、接続オブジェクトが完全に閉じられずに、次のテストケースに引きずられてしまうことがあります。
✨ 具体的な対処法(対策レベル順)
以下の手順を上から順番に試すことを強くお勧めします。ほとんどの場合、1か2の方法で解決することが期待できます。
💡 対策1:最も重要! with ステートメントの徹底利用 (Code Layer)
データベース接続やカーソルを扱う際は、必ず Python のコンテキストマネージャ(withステートメント)を使うことで、「ファイルの終了時に必ずクローズする」という安全性を確保します。
# 👎 悪い例:明示的な close が漏れるリスクがある
conn = sqlite3.connect('test.db')
cursor = conn.cursor()
# ... 処理 ...
cursor.close() # クリーンアップの忘れが怖い
conn.close()
# 👍 良い例:with を使えば、ブロックを抜けた時点で自動的にクローズされる(推奨)
import sqlite3
try:
with sqlite3.connect('test.db') as conn:
cursor = conn.cursor()
cursor.execute("...") # ここで操作を行う
# with ブロックを抜けたら、conn は確実に閉じられる
except Exception as e:
print(f"エラー処理:{e}")
💡 対策2:ファクトリ(Fixture)での明示的なクリーンアップ (Test Structure Layer)
もしテストのセットアップ(conftest.pyなど)でデータベース接続を行っている場合、フィクスチャの終了時処理 (teardown) で、確実にコネクションを破棄・削除する必要があります。
# conftest.py の例
import pytest
import sqlite3
import os
@pytest.fixture(scope="session")
def db_connection():
DB_PATH = "temp_test.db"
# --- Setup (開始時) ---
conn = sqlite3.connect(DB_PATH)
yield conn # この接続オブジェクトをテストに提供する
# --- Teardown (終了時) --- ⬅️ ここが重要!
conn.close() # まずコードレベルでクローズを試みる
if os.path.exists(DB_PATH):
os.remove(DB_PATH) # ファイル削除を試みる
ポイント:yieldを使うことで、テスト実行後の処理(Teardown)が保証されます。
💡 対策3:環境への配慮と強制解放 (Environment/Workaround Layer)
上記の方法でも解決しない場合、それはPythonやOSのロックが非常に強固になっている可能性が高いです。最終手段として、以下のいずれかを試します。
-
破棄遅延(Sleep): クリーンアップコードの直前に短い待機時間を入れることで、Windows OSにハンドルを解放する猶予を与えます。(これは根本解決ではありませんが、実用的な回避策になりやすいです。)
import time # ... テスト処理完了後 ... time.sleep(0.1) # 念のため少し待つ os.remove('test.db') -
ガベージコレクションの強制実行: Pythonがオブジェクトをメモリから完全に解放するように促します。
import gc # ... テスト処理完了後 ... del conn # 接続オブジェクトへの参照を消す gc.collect() # ガベージコレクタを実行させる os.remove('test.db')
まとめとチェックリスト
| 問題の性質 | 試すべき対策 | ポイント |
|---|---|---|
| コード実行時 のロック | 🟢 with sqlite3.connect(...) を徹底する。 |
これが最も標準的かつ安全な方法です。 |
| テスト終了後 のクリーンアップ | 🟡 Fixtureの yield 後に conn.close() と os.remove() を置く。 |
scope="session" でファイルパス管理をしっかり行いましょう。 |
| OSレベル のロック(最終手段) | 🔴 クローズ後、 time.sleep(0.1) や gc.collect() を試す。 |
本質的な解決ではないが、環境依存の問題には有効な場合があります。 |
終わりに
Google検索を駆使しても見つからなくてひとしばらく苦戦したので、もしこの記事で誰かが助かればと!