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

# 目次
- [1日目 - 初期構成](https://qiita.com/ikeikeda/items/eeed5abb2230bf031ba5)
- [2日目 - /docsの作成](https://qiita.com/ikeikeda/items/478b9e9c45b95d8bb482)
- [3日目 - DBとModelの作成](https://qiita.com/ikeikeda/items/5a12fef0fc39d8a5e1c3)
- [4日目 - CRUDの作成](https://qiita.com/ikeikeda/items/ba73f21676d315468a07)
- [5日目 - ユーザの認証の追加](https://qiita.com/ikeikeda/items/f9a738fcf2e6d6022543)

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

# テストの実行
とりあえずテストを実行するまで
## ライブラリの導入
backendの環境の中に入って以下でpytestをインストールする

```bash:bash
poetry add pytest
```

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

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

```python: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を以下のように作成する。

```python: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なので、ここでテーブルを作成する記述を追加する

```python: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})のテストを書いてみる

```python: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をつけるようなリクエストの場合は以下のように書く。

```python
  response = client.post('/users',json={
    "login_id":"test_user",
    "name":"TestUser",
    "password":"unsecurepass",
    "description":"fugafugafuga"
  })
```
multipart/form-dataなどの情報をリクエストするときはこんな感じ

```python
  response = client.post('/token',data={
    "username":"hogehoge",
    "password":"fugafuga"
  })
```

# 認証が必要なエンドポイントへのテスト
認証を通した状態を再現するためにtestconf.pyにメソッドを追加する。

```python: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へのテストを作成する

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