概要
Pytestにはpytest-bddというBDDを実装するためのプラグインがあります
今回はpytest-bddを使ったテストの書き方について説明していきます
そもそもBDDって何?
BDDは振る舞い駆動開発(Behavior-Driven Development)という開発手法の略です
名前に振る舞いという文字がある通り、コード自体の振る舞いやアプリケーションの振る舞いを焦点にテストコードを書いていく開発手法です
ちなみにBDDとともによく挙げられるTDD(テスト駆動開発、Test-Driven Development)は
テストコードを書いてから実装用のコードを書く開発手法です
前提
- WebフレームワークはDjangoを使用
- 本来であればテストコードを書いてから実装しますが説明の都合上、実装したコードをお見せしてからテストコードを記載していきます
- テストコードだけみたい方は
BDDの設定
から読んでください
- テストコードだけみたい方は
- 今回はヘルスチェックのテストをpytest-bddを使って実装していきます
ファイル構成
tree
・
├── project
│ ├── __init__.py
│ ├── asgi.py
│ ├── settings
│ ├── urls
│ └── wsgi.py
└── application
├── __init__.py
├── admin.py
├── apps.py
├── migrations
├── models.py
├── tests
| ├── features
| | └── health_check.feature
| ├── views
| | ├── __init__.py
| | └── test_health_check.py
| └── conftest.py
├── urls.py
└── views.py
├── __init__.py
└── health_check.py
初期設定
pytest-bddをインストールします
pip install pytest_bdd
実装イメージ
ヘルスチェックのテストをするには
- project/settings.py
- application/views/health_check.py
- project/urls.py
- application/urls.py
が必要になってきます
ヘルスチェックの実装
詳細は以下の記事を参考にしてください
from django.http import JsonResponse
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
@api_view(["GET"])
@permission_classes([AllowAny])
def health_check(request):
"""ヘルスチェック"""
return JsonResponse(data={"msg": "pass"}, status=status.HTTP_200_OK)
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("application.urls")),
]
from django.urls import include, path
from application.views.health_check import health_check
urlpatterns = [
path(r"", include(router.urls)),
path(r"health/", health_check, name="health_check"),
]
BDDの設定に必要なファイル
pytest-bddでテストを作成する際は
- テストケースを記載するfeatureファイル
- featureファイルをtests/フォルダの直下に作成します
- 実際のテストコードを記載するPythonファイル
の2つを用意する必要があります
では、今から実装していきましょう
BDDの実装
featureファイルの作成
今回作成するfeatureファイルは以下の通りです
Feature: ヘルスチェックAPI
Background:
Given APIClientを生成
And ヘルスチェックAPIのURL
Scenario Outline: 正常系のテスト
Given ヘルスチェック成功時のJSONを設定
When GET通信を実施
Then レスポンスのステータスが200
And 期待通りのJSONが返却されていること
Feature
まず、冒頭に
Feature: ヘルスチェックAPI
という風にこのFeatureファイル内にはどんな機能の振る舞いがあるのか記載します
今回はヘルスチェックのAPIの振る舞いをFeatureファイルに書きたいので上記のように記載しています
1つのFeatureファイル内ではFeatureは1つしか使えません
Background
Feature: ヘルスチェックAPI
Background:
Given APIClientを生成
And ヘルスチェックAPIのURL
Backgroundは後述するシナリオで共通の前提条件を定義するために使用されます
複数のシナリオで同じ前提条件を持つ時に使用すると前提条件が自動的に使用され、コード量を減らすことができます
今回はAPIと通信する時のAPIClientとヘルスチェックのURLをBackgroundとして定義しています
例えばログインする前提で使用するAPIのテストをするのであればログイン処理をBackgroundとして定義するのが良いかと思います
GivenとAndについてはScenario Outline以降で説明します
Scenario Outline
Scenario Outline: 正常系のテスト
Given ヘルスチェック成功時のJSONを設定
When GET通信を実施
Then レスポンスのステータスが200
And 期待通りのJSONが返却されていること
Scenario Outlineにテストケース名(今回だと正常系テスト)を記載します
その下にステップと呼ばれるものを設定します
Given
テストの前提条件を記載します
ヘルスチェックの正常系テストであり、成功時に期待通りのJSONが返却されていることを確認したいため、
Givenにヘルスチェック成功時のJSONを設定、と記載します
When
テストを実行するときの振る舞いを指定します
今回はヘルスチェックのAPIへGETリクエストをする、という振る舞いを行いたいので
GET通信を実施、と記載します
Then
テストが実行された時の結果(振る舞い)を記載します
ヘルスチェックのAPIへGETリクエストをする、という振る舞いを受けて200のレスポンスを返すことを期待しているので
レスポンスのステータスが200、と記載します
And
Given、When、Thenに追加のステップを行いたいときに記載します
今回はThen(テストの結果)の下に書いているので
- レスポンスのステータスが200
の他に
- 期待通りのJSONが返却されていること
というThenのステップが追加されます
ヘルスチェックのテスト
今回作成するヘルスチェックのテストは以下の通りです
from pytest_bdd import given, scenarios, then, when
from rest_framework import status
from rest_framework.test import APIClient
scenarios(
"./features/health_check.feature",
)
@given("APIClientを生成", target_fixture="client")
def client(db):
return APIClient()
@given("ヘルスチェックAPIのURL", target_fixture="url")
def health_check_url():
return "/api/health/"
@given("ヘルスチェック成功時のJSONを設定", target_fixture="expected")
def health_check_expected():
return {"msg": "pass"}
@when("GET通信を実施", target_fixture="response")
def get(client, url):
return client.get(url, format="json")
@then("レスポンスのステータスが200")
def response_status_code_200(response):
assert response.status_code == status.HTTP_200_OK
@then("期待通りのJSONが返却されていること")
def json(response, expected):
assert response.json() == expected
scenario
まず、下記のようにimportするfeatureファイルのパスを定義します
scenarios(
"./features/health_check.feature",
)
ステップ
- Given
- When
- Then
の3ステップにはそれぞれのデコレータが存在するため、
@given("APIClientを生成", target_fixture="client")
def client(db):
return APIClient()
という風に@given
デコレータの引数にfeatureファイルに記載したステップの内容と別のステップで使う際にtarget_fixtureを定義します
target_fixture
@given("APIClientを生成", target_fixture="client")
def client(db):
return APIClient()
@given("ヘルスチェックAPIのURL", target_fixture="url")
def health_check_url():
return "/api/health/"
にtarget_fixture="client"
やtarget_fixture="url"
と記載されていますが別のステップでclientを引数に入れるとその戻り値を使用することができます
以下のようにwhenのステップでtest実行時の振る舞いを記載するときにgivenで定義したfixtureを利用することができます
@when("GET通信を実施", target_fixture="response")
def get(client, url):
return client.get(url, format="json")
"/api/health/"
へGETリクエストを実施すると
@then
で記載したテスト実行後の振る舞いは
response.status_code == status.HTTP_200_OK
response.json() == expected
になればテストが成功します
@given("ヘルスチェック成功時のJSONを設定", target_fixture="expected")
def health_check_expected():
return {"msg": "pass"}
@when("GET通信を実施", target_fixture="response")
def get(client, url):
return client.get(url, format="json")
@then("レスポンスのステータスが200")
def response_status_code_200(response):
assert response.status_code == status.HTTP_200_OK
@then("期待通りのJSONが返却されていること")
def json(response, expected):
assert response.json() == expected
pytest-bddを使ったヘルスチェックのテストの解説は以上です
conftest.py
conftest.pyに他のテストでも使用するシナリオを記載することができます
- ステータスコード
- HTTPメソッド
などがよいかと思います
from pytest_bdd import given, then, when
from rest_framework import status
from rest_framework.test import APIClient
@given("APIClientを生成", target_fixture="client")
def client(db):
return APIClient()
@then("レスポンスのステータスが200")
def response_status_code_200(response):
assert response.status_code == status.HTTP_200_OK
@then("レスポンスのステータスが201")
def response_status_code_201(response):
assert response.status_code == status.HTTP_201_CREATED
@then("レスポンスのステータスが400")
def response_status_code_400(response):
assert response.status_code == status.HTTP_400_BAD_REQUEST
@when("GET通信を実施", target_fixture="response")
def get(client, url):
return client.get(url, format="json")
@when("POST通信を実施", target_fixture="response")
def patch(client, url, data):
return client.post(url, data=data, format="json")
@when("PATCH通信を実施", target_fixture="response")
def patch(client, url, data):
return client.patch(url, data=data, format="json")
@when("DELETE通信を実施", target_fixture="response")
def delete(client, url, data):
return client.delete(url, data=data, format="json")
@then("期待通りのJSONが返却されていること")
def json(response, expected):
assert response.json() == expected
と記載することでヘルスチェックのテストコードはもっと少なくなります
from pytest_bdd import given, scenarios
scenarios(
"./features/health_check.feature",
)
@given("ヘルスチェックAPIのURL", target_fixture="url")
def health_check_url():
return "/api/health/"
@given("ヘルスチェック成功時のJSONを設定", target_fixture="expected")
def health_check_expected():
return {"msg": "pass"}
このようにAPIのパスとAPIへのリクエスト成功時のJSONを記載するだけでテストを書くことができてしまいます
まとめ
初めて使った時はかなりクセが強くて慣れるのに時間がかかりましたが理屈がわかればとても楽しいですね
公式ドキュメントを読んでいてもまだまだ学ばないといけないことが多いので今後もアウトプットしていきたいと思います
参考