0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

FastAPI+React+DockerでQiitaみたいなサイトを作ってみたい -6日目-

Last updated at Posted at 2022-01-12

目次

6日目 - テストの導入

色々と仕様や導入方法などを調べていたらだいぶ時間が経ってしまったが、
前回大体の機能の実装ができたので、まずはテストを導入していく

テストの実行

とりあえずテストを実行するまで

ライブラリの導入

backendの環境の中に入って以下でpytestをインストールする

bash
poetry add pytest

テストの実行

backendの環境内に入って以下のコマンドでテストを実行できる

bash
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_***のようにするのが慣例らしい)

api/v1/tests/test_main.py
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を以下のように作成する。

api/v1/tests/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なので、ここでテーブルを作成する記述を追加する

api/v1/tests/testconf.py
# ~~~
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})のテストを書いてみる

api/v1/tests/routers/test_user.py
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にメソッドを追加する。

api/v1/tests/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へのテストを作成する

api/v1/tests/test_post.py
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の実装はあとはバリデーション周りの実装を行えば、次はフロントエンドに手をつけられそう。

0
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?