コンテンツにスキップ

単体テスト#

2024 iThome鉄人レース

これはDjango Ninjaシリーズチュートリアルの第29回です。

「プロジェクトには単体テストがありますか?」

面接でこの質問をすると、面接官が渋い顔をするかもしれません。

テストの重要性は、ほとんどの開発者が心の中でよく理解しています。しかし、真剣に取り組む意欲のある人は必ずしも多くありません。

しかし、コードの品質を向上させ、バグを減らし、プロジェクトを保守しやすくしたいと本気で考えているのであれば、単体テストは依然として不可欠なツールです。

優れたテストは問題を早期に発見するのに役立つだけでなく、プロジェクトのリファクタリングや新機能の追加時に、既存の機能が壊れないことを保証してくれます。

テストを書くことは初期の開発時間を増やし、保守にも労力を要します——これは決して簡単なことではありません。しかし長期的には、プロジェクトに持続的な健全性と安定性をもたらします。

だから、しっかりとテストを書きましょう!

GitHubサンプルプロジェクト#

👉 Django-Ninja-Tutorial


この記事の概要#

これはシリーズ全体の中で唯一、全文の概要があるチュートリアルです。

その理由は、この記事で言及すべき事項が多いからです。何しろ単体テストという大きなテーマを、たった2500文字の記事で語り尽くせるはずがありません。スペースの都合上、一つ一つ詳しく話すことはできません——しかし、完全に省略することもできません。

そのため、読者が俯瞰して理解し吸収しやすくするための全体の輪郭が必要でした。概要は以下の通りです:

  1. 単体テストの理想と現実。
  2. Django APIテストの重要な概念の説明。
    • テストクライアントの意味と用途。
    • pytestとpytest-djangoの紹介。
    • pytest fixturesとテスト関数。
  3. テストコードの実装と解説。
  4. 結語。

簡単に言うと、この記事ではすべてのコード変更を解説するのではなく、必要な時のみ言及します。残りの部分は私が直接実装してサンプルプロジェクトに収録してあるので、読者自身で参考にしてください。

限られたスペースの中で、詳細にこだわるよりも全体的な概念を理解することの方が重要です。基本的な概念をマスターすれば、コードを見る時によりスムーズに理解できるでしょう。

単体テストに関するさらなる議論については、感想記事〈なぜ単体テストを書くべきか——《Python工匠》〉を参考にしてください。これは論拠のしっかりした良書であり、きっと収穫があるはずです。

本文中のすべてのコード変更は、こちらのPRを参照してください。


1. 単体テストの理想と現実#

単体テストを議論する上で、まずは現実を直視する必要があると思います。

ソフトウェアテストの分野には、テストに関する様々な熱狂や教条主義が溢れており、時にはそれが人々を尻込みさせてしまいます。

単体テストの理想#

理論上、単体テストの作成はすべての開発者が行うべきことです(私も確かにそう思っています)。

さらに、「テスト駆動開発(TDD)」という理念もあります。これはテスト主導の開発モデルであり、機能コードを書く前にまずテストを書くことを要求します。

ごく一部の人は、テストカバレッジは100%でなければならないと考えています。なぜなら100%でなく、例えば70%だとしたら、「なぜ他の数字ではないのか?」と問うことができるからです。

現実:ほとんどの人はそんな理想を気にしない#

しかし現実には、理想的なテストを見ることは滅多にありません——テスト自体が全くないこともよくあります。

現実のプロジェクトでは、時間やリソースなど多くの制限があるため、テストの作成に労力を投入したがらない傾向があります。

古いプロジェクトの中には、初期段階でテストの基盤がなかったため、後からテストを追加することがより困難になっているものもあります(何しろめちゃくちゃな💩になっていますから)。これが私たちのよく言う「技術的負債」です。

さらに、過度な「テスト理想主義」は、初心者を尻込みさせることもあります。多くの初心者はテストに触れる時、自分が100%のカバレッジを達成できないのではないかと心配し、テストに対する抵抗疑念を抱きます。

このような完璧主義は往々にして有害無益です。私たちは理想と現実の間に妥協点を見つける必要があります。

妥協点:実用的なテスト戦略#

実際の開発では、私たちは実用的で実行可能なテスト戦略を堅持すべきです。APIの呼び出しと200レスポンスなど、プロジェクトの最もコアな機能のテストに焦点を当てます。

多くの場合、60-70%の機能をカバーできれば、プロジェクトのコード品質を明らかに向上させ、後続の開発に一定の安心感を提供することができます——これは本当に重要です。

完璧なテストカバレッジを追求する必要はありません。行動を始める意欲さえあれば、テストは本来の価値を発揮することができます。


2. Django APIテストの重要な概念の説明#

プロジェクトそのものに戻りましょう。

この記事でAPI単体テストの具体的な詳細に多く言及することはできませんが、重要な概念は省略できません。以下に一つ一つ説明します。

テストクライアントの意味と用途#

Test clientはAPIのテストにおいて極めて重要です。なぜなら、それが実際のHTTPリクエストをシミュレートできるからです——注意してください、あくまでシミュレートです。

APIテストと一般的なコードテストは少し異なります。一般的なテストは、関連するテスト関数やロジックを書いて実行するだけで済みます。しかしAPIテストでは、リクエストの送信をシミュレートするための「偽のクライアント」が必要です。

APIを手動でテストする場合、通常はPostmanなどのAPIクライアントを使用します。しかし自動化された単体テストでは、この「偽のクライアント」を直接テストコード内に書く必要があります——それがtest clientです。

これは「プロジェクト内部のAPIクライアント」に相当し、しかも自動で実行できます。

Django Ninjaは独自のtest clientを提供していますが、まだ十分に健全ではないため、最初は使用しないことをお勧めします。

サンプルプロジェクトでは、Django組み込みのtest clientを使用しています——歴史が長く、安定して信頼性があります。

pytestの紹介#

pytest(そうです、pは小文字です。pyenvと同じです)は広く人気のあるPythonテストフレームワークであり、多数の実用的なプラグインを含む独自のエコシステムを持っています。

Python組み込みのunittestモジュールと比較して、pytestの構文はより直感的で、柔軟性に優れています。特にそのfixturesパラメータ化テストなどの機能により、テストの作成がより簡単かつ効率的になります。

pytest-django#

pytest-djangoは、Djangoのために特別に設計されたpytestの統合パッケージです。多くの組み込みfixturesや実用的なデコレータを含む、豊富なDjango統合機能を提供します。

中でも@pytest.mark.django_dbデコレータが最もよく使用され、テスト中のデータベースの状態を自動的に管理することができます。

これにより、pytestは各テストの実行前に全く新しいデータベースを自動的に作成し、テスト終了後にそれを削除します。これにより各テストの環境の一貫性が保証され、データの残留による不正確なテスト結果を防ぐことができます。

pytest Fixturesとテスト関数#

Fixturesはpytestが提供するメカニズムであり、テストに必要な初期環境設定に使用されます。それらは本質的には関数ですが、一般的な関数のようには使用されません。あらかじめ定義しておくだけで、テスト関数内でパラメータとして参照することができます。

FixturesはDjangoアプリのtests.py内に定義することもできますが、私たちは通常、プロジェクト全体で共有できるようにconftest.pyモジュールに配置します。

APIをテストする時、ユーザーや製品などの初期データがしばしば必要になります。これらのデータは毎回手動で再構築することなく、fixturesを通じて自動的に生成することができます。

これにより、テストを書く効率が向上し、状態設定の繰り返しも回避されます。


3. テストコードの実装と解説#

この記事では3つのfixtureと3つのテスト関数を実装します。それらはすべてuserに関連しています。詳細の中から要点を選んで解説させてください。

強力で柔軟なpytest Fixtures#

これはプロジェクトの3つのfixtureで、conftest.py内に定義されています:(長さを減らすためにdocstringを省略しました)

import pytest
from django.test import Client
...

@pytest.fixture(scope='session')
def client() -> Client:
    return Client()

@pytest.fixture
def user() -> User:
    return User.objects.create_user(
        username='testuser',
        email='testuser@example.com',
        password='testpassword123'
    )

@pytest.fixture
def authenticated_client(client: Client, user: User) -> Client:
    response = client.post(
        '/users/login/',
        {'username': 'testuser', 'password': 'testpassword123'},
        content_type='application/json',
    )
    assert response.status_code == 200
    # ログイン後のcookiesを設定
    client.cookies.update(response.cookies)
    return client

このコードにおいて:

  1. client:リクエストの送信をシミュレートするために使用できるDjango test clientを提供します。(未認証)
  2. user:テスト関数や他のfixtureから参照できるように、テスト用のユーザーを自動的に作成します。
  3. authenticated_client:上記の2つのfixtureを参照し、ログイン済みのclientを組み合わせてシミュレートします。これにより、「認証保護」のあるAPIをテストできるようになります。

Fixturesの定義、組み合わせ、そして使用は、pytestの大きな特徴です。

テストの環境設定を簡素化できるだけでなく、テストコードの可読性を高めることもできます——テスト状態テストロジックを分離することであり、これも「関心の分離」の一種です。

実際のテスト関数では、必要なfixturesをパラメータとして渡すだけで済みます。pytestはそれらの初期化とクリーンアップ作業を自動的に処理します。

この設計はコードの重複を大幅に減らし、テストが環境設定よりもAPIの論理検証に集中できるようにします。

テスト関数#

最後にテスト関数ですが、そのうちの2つだけを見てみましょう:(fixtures自体に焦点を当ててもらうため、パラメータの型ヒントは省略しました)

def test_get_users(authenticated_client) -> None:
    """
    すべてのユーザーの取得をテスト
    """
    response = authenticated_client.get('/users/')
    assert response.status_code == 200

def test_login_user(client, user) -> None:
        """
    ユーザーログインをテスト
    """
    response = client.post(
        '/users/login/',
        data={'username': 'testuser',
              'password': 'testpassword123'},
        content_type='application/json',
    )
    assert response.status_code == 200

これら2つの関数を選んだのには教える意図があります:

  1. test_login_user関数は「ユーザーログイン」APIをテストします。このAPIは「未ログイン」のユーザーがアクセスするためのものなので、一般的なclient(未認証)を参照するだけで済みます。
    • この関数はuser fixtureも参照しています。なぜならログイン成功の前提は、そのユーザーがすでに「存在している」ことだからです。
    • そしてuser fixtureの役割はまさに、テスト開始前にそのユーザーを先に作成することです。
  2. test_get_usersがテストするのは「認証保護がある」APIであり、アクセスにはログインが必要なため、authenticated_clientを参照しました。
    • このテスト関数はauthenticated_client「のみ」を参照していますが、実際のテスト結果は、リストにユーザーが存在している状態になります。(コードにはこの部分は含まれていません)
    • なぜならauthenticated_clientはuserとclientの2つのfixtureを参照しているからです。したがってauthenticated_clientを参照するだけで上記2つを参照したのと同じことになります。

どのfixtureを「参照」すべきかは、各関数がどのようなテスト状態と条件を必要とするかによって決まります。

Fixtures自体を再利用できるこの設計により、テスト自体も非常に「モジュール化」されます——これがpytestがこれほど人気がある理由の一つです。

単体テストの実行#

最後に、テストを実行してみましょう!

プロジェクトのルートディレクトリで直接pytestコマンドを使用するか、VS CodeのTesting UIから単体テストを実行できます:

VS Code - Testing

Beautiful!


4. 結語#

理想と現実には常にギャップがありますが、実用的なテスト戦略を通じて、完璧を過度に追求することなく、プロジェクトに十分な品質保証を提供することができます。

Test clientやpytestなどのツールにより、APIテストは簡単で理路整然としたものになります。テストカバレッジは100%である必要はありません。一定のレベルに達することができれば、開発プロセスに大きな助けをもたらすことができます。

このシリーズチュートリアルもいよいよ大詰めです。Django Ninjaのコア機能から高度な機能まで、ルーティング設計から単体テストまでを探求してきました。読者にとっても私にとっても、大変ながら充実した道のりでした。

次回が最終回です。シリーズ全体を簡単に振り返り、今回の鉄人レースを執筆し、完走した感想を共有します。