どのようなプログラミング言語でも、テスト作成はハードルが高いものです。しかし、適切な手法さえ選べば、決して難しいものではありません。 この Flask のチュートリアルでは、Flask と Pytest を使用すれば、テストを簡単に作成して実行できることを紹介します。 詳しい内容は次のとおりです。

  1. Flask アプリで Pytest をセットアップする
  2. Pytest テストを作成する
  3. テストをグループ化する

さらに、CircleCI を使用して、Flask アプリのテストを実行する CI/CD パイプラインを組み込む方法についても説明します。

前提条件

この Flask チュートリアルは、以下を身に付けたうえで取り組むと、よりいっそう理解が深まります。

  • Python プログラミング言語の基礎知識
  • Flask フレームワークの知識
  • テストの基礎知識

また、以下の準備も必要です。

  1. Python バージョン 3.5 以上をインストールする
  2. GitHub アカウントを こちらで作成する
  3. CircleCI アカウントを こちらで作成する
  4. GitHub プロジェクト リポジトリをこちらからクローンする

プロジェクトをクローンしたら、プロジェクトのルート フォルダーで pip install -r requirements.txt コマンドを実行し、依存関係をインストールしてください。

それでは、チュートリアルを始める前に、まずは Flask の概要と仕組みを簡単に確認しておきましょう。

Flask とは?

Flask は、Python で開発された軽量なマイクロ Web フレームワークです。 Flask にプリインストールされているのはコア機能のみで、プロジェクトの要件に応じてカスタマイズできるようになっています。 使用するデータベースやデフォルト パーサーは決められておらず、 開発者が変更できるようになっています。このように、Flask は開発チームに新たなニーズが生じても対応できる拡張性と柔軟性を備えています。

このチュートリアル プロジェクトでは、Flask で開発したシンプルな book-retrieval API を使用します。 この API をもとに、Flask アプリケーションのテストを作成する方法について説明します。 今回は説明をシンプルにするために、API の開発方法は省略して、テストだけを重点的に取り上げます。 この API アプリケーションのコードは、先ほどクローンしたリポジトリのプロジェクト ルート ディレクトリ内にある、api.py ファイルです。

以上が、Flask の概要と用途です。次は、Pytest の概要と Flask アプリのテストのセットアップ方法を確認して、さらに知識を深めましょう。

Pytest とは?どのように使う?

Pytest は、開発の効率化とリリースの信頼性強化を目的とした、開発者向けの Python テスト フレームワークです。 Pytest を使用すると、拡張性の高い小規模テストを簡単に作成できます。 Pytest テストの基本的な形式は、下記のコード スニペットのようになります。

from api import app # API の Flask インスタンス

def test_index_route():
    response = app.test_client().get('/')

    assert response.status_code == 200
    assert response.data.decode('utf-8') == 'Testing, Flask!'

Flask をテストするには、上記スニペットのように、まず (アプリケーション内で作成済みの) api から Flask インスタンス app をインポートする必要があります。 次に、インポートしたインスタンスで、Flask の test_client() メソッドを公開します。このメソッドには、テスト対象のアプリケーションに対して HTTP リクエストを送信するための機能が含まれています。 今回のリクエストの送信先は、デフォルト (/) の API エンドポイントです。 その後、バイト オブジェクトをデコードして、test_client() が受け取った応答が期待どおりのものであるか確認します。 次のセクションで、この形式をもとにテストを作成します。

注: デフォルトでは、Pytest は API 応答を utf-8 コーデックでエンコードします。 そのため、上述のコード スニペットのように、テスト クライアントから受け取った バイト オブジェクトを decode() メソッドで変換して、判読できる文字列にする必要があります。

Flask の Pytest テストをセットアップする

Pytest のインストールは簡単です。 リポジトリをクローンした場合は、Pytest もインストール済みなので、次の手順に進んでください。 リポジトリをクローンしていない場合は、以下のコマンドをターミナルで実行します。

 pip install pytest

注: 仮想環境を使用している場合は、インストール前にその環境を有効化してください。 ベスト プラクティスとして、アプリケーションおよび関連する Python パッケージごとに、仮想環境を作成することをお勧めします。

Pytest モジュールをインポートする

インストールが完了したら、Pytest モジュールをインポートします。 手順は、テスト ファイルに次のスニペットを追記すれば完了です。

import pytest

テストの命名規則

このチュートリアルでは、テストをアプリケーションのルート フォルダー内の tests ディレクトリに配置します。 Pytest を使用する場合は、テスト ファイル名の先頭か末尾に「test」と付ける (test_*.py または **_test.py) ことが推奨されています。この規則に従うことで、Pytest でテスト ファイルが自動的に検出されやすくなり、テスト実行時の混乱も抑えられます。 さらに、テストの作成時には、テスト メソッド名の先頭に test を付けるようにします (例: test_index_route())。

Pytest を使用してテストを作成する

Pytest で Flask アプリのテストをセットアップする方法がわかったところで、book-retrieval API のテストの作成に取りかかりましょう。 最初に作成するテストは、以下に示す /bookapi/books ルートに対するものです。

# 書籍のデータを取得

books = [
    {
        "id": 1,
        "title": "CS50",
        "description": "Intro to CS and art of programming!",
        "author": "Havard",
        "borrowed": False
    },
    {
        "id": 2,
        "title": "Python 101",
        "description": "little python code book.",
        "author": "Will",
        "borrowed": False
    }
]

@app.route("/bookapi/books")
def get_books():
    """ すべての書籍を取得する関数 """
    return jsonify({"Books": books})

このルートの処理は、books 変数にハードコーディングされた書籍のリストを返すことだけです。 それでは、このエンドポイントのテストを作成します。 前セクションの形式に従ってテスト関数を定義し、test_client() で受け取った応答が期待どおりのものであるかを確認します。

import json
from api import app

def test_get_all_books():
    response = app.test_client().get('/bookapi/books')
    res = json.loads(response.data.decode('utf-8')).get("Books")
    assert type(res[0]) is dict
    assert type(res[1]) is dict
    assert res[0]['author'] == 'Havard'
    assert res[1]['author'] == 'Will'
    assert response.status_code == 200
    assert type(res) is list
    ....

このテスト スニペットでは、書籍のリストを受け取れたかどうかを確認します。 そのために、test_client() が受け取った応答が、期待される書籍のリストであるか検証します。 また、応答に辞書型のリストが含まれるか検証します。この場合の辞書は、定義した書籍オブジェクトに含まれる個別の書籍を表します。 最後に、リストの最初の書籍の著者が Havard であり、2 つ目の書籍の著者が Will であるか検証します。 これで、/bookapi/books エンドポイントからすべての書籍を取得するテストを作成するための情報がわかりました。

さっそく、このテストが成功するか確認しましょう。そのために、まず Pytest でテストを実行する方法を確認します。

Pytest を使用してテストを実行する

Pytest のテストを実行するには、ターミナルで py.test または pytest を実行します。 pytest コマンドの後にファイル名を明示的に指定することで、1 つのテスト ファイルだけを実行することもできます (例: pytest test_api.py)。 これらのコマンドを実行すると、Pytest により、ルート ディレクトリ内または指定した 1 つのファイル内に含まれるすべてのテストが自動的に検出されます。

テスト ファイルを明示的に指定しない場合、ルート ディレクトリ内にある標準命名規則に基づくすべてのテストが実行されます。ファイル名やディレクトリを指定する必要はありません。

最初のテストを実行する

テストが正常に実行されことを確認できました。 Pytest では、合格したテストには緑の点 . が付き、失敗したテストには赤で F と表示されます。 点と F の数を数えることで、合格したテスト数、失敗したテスト数、テストの実行順序がわかります。

注: コンソールでテストをデバッグしていて、応答を出力する必要がある場合は、$ pytest -s test_*.py で標準出力にログを出力できます。 Pytest の s オプションを指定すると、テスト内のメッセージをコンソールに出力して、テストの実行中にその出力をデバッグできます。

最初のテストを無事に実行できたので、次は CircleCI を使用し、main ブランチへのプッシュのたびにテストを自動実行するように設定しましょう。

Git をセットアップして CircleCI にプッシュする

CircleCI をセットアップするには、以下のコマンドを実行してプロジェクトの Git リポジトリを初期化します。

git init

初期化が完了したら、ルート ディレクトリに .gitignore ファイルを作成します。 このファイルには、無視するモジュールを追加します。 このファイルに追加したモジュールは、リモート リポジトリに追加されなくなります。

次の手順として、コミットを追加し、プロジェクトを GitHub にプッシュします

CircleCI にログインして、[Projects (プロジェクト)] ダッシュボードに移動します。 お使いの GitHub ユーザー名または組織に関連付けられている GitHub リポジトリの中から、CircleCI でセットアップするリポジトリを選択します。 今回は、testing-flask-framework-with-pytest です。 [Projects (プロジェクト)] ダッシュボードで、[Set Up Project (プロジェクトのセットアップ)] を選択します。 既存の設定ファイルを使用するオプション ([Fastest (最速)]) を選択し、ビルドを開始します。

ビルドが始まり、パイプラインが失敗します。これは、カスタマイズした .circleci/config.yml 設定ファイルを GitHub に追加していないからです。 ビルドを正常に実行するために、設定ファイルを追加しましょう。

CircleCI をセットアップする

ルート ディレクトリに .circleci ディレクトリを作成し、ディレクトリ内に config.yml ファイルを追加します。 設定ファイルには、すべてのプロジェクト用の CircleCI 設定を記述します。 今回は、テストを実行するために CircleCI の Python Orb を指定します。

version: 2.1
orbs:
  python: circleci/python@1.4.0

workflows:
  sample:
    jobs:
      - build-and-test
jobs:
  build-and-test:
    docker:
      - image: cimg/python:3.8
    steps:
      - checkout
      - python/install-packages:
          pkg-manager: pip
      - run:
          name: テストの実行
          command: python -m pytest

CircleCI の Orb とは、YAML コードを再利用可能なパッケージにまとめたものです。 Orb を使用することで、複数行のコードを 1 行に凝縮できます (上記の例では python: circleci/python@1.4.0)。 CircleCI Orb を使用するために、CircleCI ダッシュボードの [Organization Settings (組織設定)] でサードパーティ製 Orb の使用を許可するか、組織の CircleCI 管理者にアクセス許可をリクエストする必要がある場合があります。

設定ファイルが完成したら、GitHub にプッシュします。 これで、CircleCI でプロジェクトのビルドが自動的に始まります。

うまくいきました! CircleCI ダッシュボードで、ビルドをクリックして詳細を確認しましょう。 最初の Pytest テストが実行され、CircleCI への組み込みが成功したことがわかります。

パイプラインのセットアップ成功

継続的インテグレーションのセットアップに成功したので、次はテストをグループ化して一括で実行してみましょう。

テストをグループ化して一括実行する

アプリケーションが問題なく動作することを確認するには、機能を追加するたびにテストも増やす必要があります。 そのため、テスト スクリプトの数が増えすぎて、手に負えなくなりがちです。 でも、心配はいりません。Pytest にはその対策が用意されています。 複数のテストを 1 つのファイルにグループ化して、まとめて実行できるのです。

具体的には、Pytest の マーカーを使用して、テスト関数に属性と機能を設定します。

Pytest のテスト マーカーを使用する

テスト マーカーを使用すると、テストにさまざまな動作を指定できます。たとえば、テストを省略したり、テストの一部のみを実行したり、テストを強制的に失敗させることができます。 Pytest のデフォルト マーカーには、xfailskipparameterize などがあります。 このプロジェクトでは、カスタム マーカーを作成して、/bookapi/books エンドポイントと /bookapi/book/:id エンドポイントに対して GET リクエストを行うすべてのテストをグループ化します。

カスタム マーカーを作成するためのテンプレートは次のとおりです。

@pytest.mark.<markername>
def test_method():
  # テスト コード

Pytest でカスタム マーカーを使用するには、以下のように pytest コマンドの引数として指定します。

$ pytest -m <markername>

<markername> に、テストで使用するカスタム マーカー名を指定します。

Pytest マーカーを使用するには、テスト ファイルで pytest をインポートする必要があることに注意してください。

また、未登録のマーカーを使用すると Pytest で警告が表示されます。警告を抑制するには、 pytest.ini ファイルに以下を追加します。

[pytest]
markers =
    <markername>: マーカーの説明

マーカーを使用してテストをグループ化する

このチュートリアルでは、/bookapi エンドポイントに対して GET リクエストを行うテストをグループ化します。

以下のように、get_request という名前のカスタム マーカーを作成して、このマーカーをテストに追加します。

import pytest
...
# その他のインポートをここに追加

@pytest.mark.get_request
def test_get_book_by_id():
    response = app.test_client().get('/bookapi/books/1')
    res = json.loads(response.data.decode('utf-8')).get("Book")
    print(res)
    assert res['id'] == 1
    assert res['author'] == 'Havard'
    assert res['title'] == 'CS50'
    assert response.status_code == 200

pytest -m get_request のように引数を指定してテストを実行すると、テスト ファイルに含まれるテストのうち、@pytest.mark.get_request デコレーターでマークされているすべてのテストが実行されます。 実際にターミナルでコマンドを実行して、確認してみてください。

では、変更をコミットして GitHub にプッシュし、パイプラインが正常に実行されることを確認しましょう。

テスト成功

これで、すべてのテストが正常に実行されました。

おわりに

このチュートリアルでは、Pytest を使用してテストを作成および実行する方法と、Pytest マーカーでテストをグループ化する方法を学びました。 また、Pytest のコマンドライン引数を使用してテストを実行する方法や、test_client() メソッドを使用して HTTP リクエストを行う方法、受け取った応答をテストで使用する方法についても学びました。 このチュートリアルがお役に立ったなら幸いです。 ぜひ、学んだ内容をチームで共有してください。 学んだことをしっかりと身につけるには、他の人に教えるのが一番です。 次の記事でまたお会いしましょう。それまでは、みなさんのコーディング プロジェクトをお楽しみください。


Waweru Mwaura 氏は品質工学を専門とするソフトウェア エンジニアです。生涯学習の実践者という顔も持ち、 Packt で執筆者を務めながら、工学や金融、テクノロジーの書籍を愛読しています。 Mwaura 氏の詳細な情報は、こちらの Web プロフィールをご覧ください。

さんの他の投稿を読む Waweru Mwaura