目次
6日目 - テストの導入
色々と仕様や導入方法などを調べていたらだいぶ時間が経ってしまったが、
前回大体の機能の実装ができたので、まずはテストを導入していく
テストの実行
とりあえずテストを実行するまで
ライブラリの導入
backendの環境の中に入って以下でpytestをインストールする
poetry add pytest
テストの実行
backendの環境内に入って以下のコマンドでテストを実行できる
poetry run pytest
テスト前の準備
今までずっと無視していたがpytestを行う上で各ディレクトリに__init__.py(ディレクトリをライブラリとして認識するための空ファイルらしい)を作成する必要があるので、今まで作った以下のディレクトリに__init__.pyの空ファイルを作っておく。
- backend/api/__init__.py
- backend/api/v1/__init__.py
- backend/api/v1/tests/__init__.py
テストの作成
一旦/のルーティングにあるhalloworldのレスポンスを返すエンドポイントのテストを書いていく
テストファイルを以下のように作成する。
(テストファイルはファイル名をtest_***のようにするのが慣例らしい)
from fastapi.testclient import TestClient
from api.v1.main import app
client = TestClient(app)
def test_root_path():
response = client.get('/')
assert response.status_code == 200
FastApiで用意されているTestClientを利用してappに対して各リクエストをテストし、responseの内容をチェックしていくことでテストを行なっていくことができる。
データベースを利用したテスト
データベースを利用したテストを行なっていく場合、テスト用のDBを用意する必要がある。
今回はsqliteを利用する。
今までrouter内でDBを利用する際はget_dbというメソッドを依存注入していたのがここで効いてくる。
test用のconfファイルとしてtestconf.pyを以下のように作成する。
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import create_engine
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.orm.session import close_all_sessions
from api.v1.main import app
from api.v1.db import get_db
class TestingSession(Session):
def commit(self): # commitをオーバーライドする
self.flush()
self.expire_all()
@pytest.fixture(scope="function")
def test_db():
engine = create_engine("sqlite:///./test_sqlite.db",connect_args={"check_same_thread":False})
TestSessionLocal = sessionmaker(class_=TestiongSession,autocommit=False,autoflush=False,bind=engine)
db = TestSessionLocal()
def get_test_db():
try:
yield db
db.commit()
except SQLAlchemyError as e:
assert e is not None
db.rollback
app.dependency_overrides[get_db] = get_test_db
yield db
db.rollback()
close_all_sessions()
engine.dispose()
これでこのメソッドを先程のTestClientから作成したclientの引数に撮ることでget_dbをオーバーライドしてテスト時はsqliteを利用するように変更することができる。
テスト用のDBに各テーブルを作成する
このままだと何のテーブルもないDBなので、ここでテーブルを作成する記述を追加する
# ~~~
from api.v1.models import user, post, lgtm
# ~~~
def test_db():
engine = create_engine("sqlite:///./test_sqlite.db",connect_args={"check_same_thread":False})
user.Base.metadata.create_all(bind=engine)
post.Base.metadata.create_all(bind=engine)
lgtm.Base.metadata.create_all(bind=engine)
TestSessionLocal = sessionmaker(class_=TestingSession,autocommit=False,autflush=False,bind=engine)
# ~~~
これで各Modelに対応するテーブルが作成できる
テストを作成
DBを利用したエンドポイント(GET /user/{login_id})のテストを書いてみる
from fastapi.testclient import TestClient
from api.v1.main import app
from api.v1.tests.testconf import test_db
from api.v1.models.user import User
client = TestClient(app)
def test_get_user_path(test_db):
user1 = User(login_id="test_user_1",name="TestUser1",password_hash="unsecurepass")
user2 = User(login_id="test_user_2",name="TestUser2",password_hash="unsecurepass")
test_db.add_all([user_1,user_2])
response = client.get(f'/user/{user1.login_id}')
assert response.status_code == 200
assert response.json()["login_id"] == "test_user_1"
assert response.json()["name"] == "TestUser1"
assert response.json()["password_hash"] is None
これでDBを利用したケースのテストが書けるようになった。
ちなみにPostメソッドなどのリクエストにbodyをつけるようなリクエストの場合は以下のように書く。
response = client.post('/users',json={
"login_id":"test_user",
"name":"TestUser",
"password":"unsecurepass",
"description":"fugafugafuga"
})
multipart/form-dataなどの情報をリクエストするときはこんな感じ
response = client.post('/token',data={
"username":"hogehoge",
"password":"fugafuga"
})
認証が必要なエンドポイントへのテスト
認証を通した状態を再現するためにtestconf.pyにメソッドを追加する。
# ~~~
import api.v1.cruds.auth as crud_auth
# ~~~
def authorized_client(client:TestClient,user.User):
access_token = crud_auth.create_access_token(data={"sub":user.login_id})
client.headers = {
**client.headers,
"Authorization":f"Bearer {access_token}"
}
return client
def deauthorized_client(client:TestClient):
del client.headers["Authorization"]
return client
テストの作成
認証が必要なPost /postsへのテストを作成する
from fastapi.testclient import TestClient
from api.v1.main import app
from api.v1.tests.testconf import test_db,authorized_client,deauthorized_client
from api.v1.models.user import User
from api.v1.models.post import Post
client = TestClient(app)
def test_post_posts_path(test_db):
user_1 = User(login_id="test_user1",name="TestUser1",password_hash="unsecurepass")
test_db.add(user_1)
test_db.flush()
test_db.commit()
authorized_client(client,user_1)
before_count = test_db.query(Post).count()
response = client.post("/posts",json={
"title":"hogehoge",
"context":"fugafugafugafuga"
})
after_count = test_db.query(Post).count()
assert response.status_code == 200
assert after_count-before_count == 1
assert response.json()["title"] == "hogehoge"
assert response.json()["context"] == "fugafugafugafuga"
assert response.json()["user"]["login_id"] == "test_user1"
deauthorized_client(client)
これで認証を通した状態でのテストが書けるようになった。
大体のテストはこのパターンで書けるので、これを参考に他のエンドポイントへのテストを追加していく。
backendの実装はあとはバリデーション周りの実装を行えば、次はフロントエンドに手をつけられそう。