テスト駆動開発まとめ【Python】

皆さんはテスト駆動開発(Test-Driven Development; TDD)を聞いたことがありますか?

少し難しそうに聞こえますが、中身を理解すると意外とシンプルな開発手法になります。

私自身、ここ最近テストコードを書くことが増え、改めてオライリーのTDDの本を通して、学び直したため、この記事を書こうと思いました。

テスト駆動開発 / TDDとは

テスト駆動開発(Test-Driven Development; TDD)とは、「テストファーストの開発手法」です。

TDDの具体的な開発の流れは下記の通りです。

  1. 要件を整理
  2. テストコードを実装
  3. 実行して失敗(レッド)
  4. エラーがなくなるようにメインコードを実装
  5. 実行して成功(グリーン)
  6. リファクタリング

上記のような流れを「Red / Green / Refactoring」と言ったりします。Redはテストの失敗を表し、Greenはテストの成功を表します。

テスト駆動開発のメリット

テスト駆動開発のメリットは主に3つあります。
決して、テスト駆動開発において特有のメリットではありませんが、これらの恩恵は大きいです。

  • テストコードを実装する過程で、仕様を明確に理解する機会ができる。
  • まず動くコードを書いてから、綺麗にするため、実装のアジリティ(迅速性)がある。
  • エラーの原因が特定しやすい。

最後の「エラーの原因が特性しやすい」という点に関して補足をすると、テスト駆動開発では、通常のエラー文だけでなく「想定した結果と実際の結果の差分」がログとして出力されるということがエラーの原因特定につながりやすいと考えられます。

テスト駆動開発のデメリット

では、テスト駆動開発は必ず実行すべきなのでしょうか?
具体的なデメリットとしては主に3つあります。

  • テストケースの漏れがあると、バグに気付かずに実装が進んでしまう。
  • 実行時間のかかるテストコードがあると、デバッグに時間を要してしまう。
  • テストがしづらい機能の場合、時間がかかるケースがある。

本来、テストコード(特にユニットテスト)では、実行時間がものすごくかかることは稀です。もしもデータベースなどとの接続をしてしまっている場合は、mockなどを用いて処理と分離されることを推奨します。

テスト駆動開発を採用するべき条件

上記のメリットとデメリットを考慮すると、以下のようにテスト駆動開発を採用するべきだと考えられます。

  • 高い品質のコードが求められ、テストコードを下流の工程で実装する予定があるケース。
  • テストコードが実装しやすい機能であるケース。
  • アジャイル開発の手法が採用されているケース。

実際にPythonを使った流れ

1. 要件を整理

まずは、要件を整理しましょう。
今回は、とてもシンプルに「メールアドレスかを検証する関数(validate_email)」を作成しましょう。

下記の要件を満たす関数を作っていきましょう。
本来は、より厳密な条件を満たす必要がありますが、今回は簡易的に以下の仕様とします。

  • 関数名は、”validate_email“。
  • 「”@” を含む」と「” . ” を含む」の二つの条件を満たす関数。
  • 引数として、メールアドレスを文字列で渡す。
  • 返り値として、条件を満たすかという結果を真偽値で返す。

2. テストコードを実装

まず、以下のような空のメインコードを書きましょう。
ここに検証をする関数をこの後書いていきます。

validate_email.py
def validate_email(): return

その後、以下のようなテストコードを書いていきます。今回の要件に沿ったテストコードです。

test_validate_email.py
import unittest
from validate_email import validate_email
class TestValidateEmail(unittest.TestCase): def test_validate_email(self): self.assertEqual(validate_email('sample@gmail.com'), True) self.assertEqual(validate_email('sample_gmail.com'), False) self.assertEqual(validate_email('sample@gmail_com'), False) self.assertEqual(validate_email('sample_gmail_com'), False)
if __name__ == '__main__': unittest.main()

3. 実行して失敗(レッド)

上記の2ファイルが同じディレクトリに配置された状態で、以下のように実行します。

% python3 test_validate_email.py

すると、以下のように、エラーが出力されると思いますが、問題ありません。
TDDにおける開発は、ここからスタートします。

======================================================================
ERROR: test_validate_email (__main__.TestValidateEmail)
----------------------------------------------------------------------
Traceback (most recent call last): File "/Users/user_name/xxx/test_validate_email.py", line 8, in test_validate_email self.assertEqual(validate_email('sample@gmail.com'), True)
TypeError: validate_email() takes 0 positional arguments but 1 was given
----------------------------------------------------------------------
Ran 1 test in 0.001s
FAILED (errors=1)

4. エラーがなくなるようにメインコードを実装

こうして、エラーが出力されたため、エラーが解消されるようにメインコードを実装していきましょう。最初のエラーは、TypeError: validate_email() takes 0 positional arguments but 1 was givenと出力されており、引数がないことがわかります。

このように、「開発・テスト・エラーを確認」というサイクルを回していきます。

最終的に、以下のようなコードが完成しました。

validate_email.py
def validate_email(email): if '@' in email and '.' in email: return True else: return False

5. 実行して成功(グリーン)

再度、以下のコマンドを実行しましょう。

% python3 test_validate_email.py

すると、以下のように成功したことがわかります。

----------------------------------------------------------------------
Ran 1 test in 0.000s
OK

6. リファクタリング

ここまで、なるべくアジャイルに開発することがポイントです。
そして、テストが通ったら、コードのリファクタリングを実施しましょう。

ここでは、リファクタリングとして次のようなことを実施しました。

  • 処理を簡略化
  • 引数と返り値のデータ型の明示
  • docstringの記載

validate_email.py
def validate_email(email: str) -> bool: """Validate email address. Args: email (str): Email address to validate. Returns: bool: True if email is valid, False otherwise. """ return '@' in email and '.' in email

最後に

いかがだったでしょうか?

少しでもこの記事を通して、TDDを身近に感じてもらえたら幸いです。

この他にも、Twitterにて「データサイエンティスト / エンジニアに役立つ情報を発信中」です。ご興味があれば、ぜひフォローお待ちしております。

参考文献