はじめまして、フリーランスのますみです!
『一人一人が自立・共存・革新している「クリエイターエコノミー」を創る。』というビジョンに向けて活動しています。
皆さんはテスト駆動開発(Test-Driven Development; TDD)を聞いたことがありますか?
少し難しそうに聞こえますが、中身を理解すると意外とシンプルな開発手法になります。
私自身、ここ最近テストコードを書くことが増え、改めてオライリーのTDDの本を通して、学び直したため、この記事を書こうと思いました。
テスト駆動開発 / TDDとは
テスト駆動開発(Test-Driven Development; TDD)とは、「テストファーストの開発手法」です。
TDDの具体的な開発の流れは下記の通りです。
- 要件を整理
- テストコードを実装
- 実行して失敗(レッド)
- エラーがなくなるようにメインコードを実装
- 実行して成功(グリーン)
- リファクタリング
上記のような流れを「Red / Green / Refactoring」と言ったりします。Redはテストの失敗を表し、Greenはテストの成功を表します。
テスト駆動開発のメリット
テスト駆動開発のメリットは主に3つあります。
決して、テスト駆動開発において特有のメリットではありませんが、これらの恩恵は大きいです。
- テストコードを実装する過程で、仕様を明確に理解する機会ができる。
- まず動くコードを書いてから、綺麗にするため、実装のアジリティ(迅速性)がある。
- エラーの原因が特定しやすい。
最後の「エラーの原因が特性しやすい」という点に関して補足をすると、テスト駆動開発では、通常のエラー文だけでなく「想定した結果と実際の結果の差分」がログとして出力されるということがエラーの原因特定につながりやすいと考えられます。
テスト駆動開発のデメリット
では、テスト駆動開発は必ず実行すべきなのでしょうか?
具体的なデメリットとしては主に3つあります。
- テストケースの漏れがあると、バグに気付かずに実装が進んでしまう。
- 実行時間のかかるテストコードがあると、デバッグに時間を要してしまう。
- テストがしづらい機能の場合、時間がかかるケースがある。
本来、テストコード(特にユニットテスト)では、実行時間がものすごくかかることは稀です。もしもデータベースなどとの接続をしてしまっている場合は、mockなどを用いて処理と分離されることを推奨します。
テスト駆動開発を採用するべき条件
上記のメリットとデメリットを考慮すると、以下のようにテスト駆動開発を採用するべきだと考えられます。
- 高い品質のコードが求められ、テストコードを下流の工程で実装する予定があるケース。
- テストコードが実装しやすい機能であるケース。
- アジャイル開発の手法が採用されているケース。
実際にPythonを使った流れ
1. 要件を整理
まずは、要件を整理しましょう。
今回は、とてもシンプルに「メールアドレスかを検証する関数(validate_email)」を作成しましょう。
下記の要件を満たす関数を作っていきましょう。
本来は、より厳密な条件を満たす必要がありますが、今回は簡易的に以下の仕様とします。
- 関数名は、”validate_email“。
- 「”@” を含む」と「” . ” を含む」の二つの条件を満たす関数。
- 引数として、メールアドレスを文字列で渡す。
- 返り値として、条件を満たすかという結果を真偽値で返す。
2. テストコードを実装
まず、以下のような空のメインコードを書きましょう。
ここに検証をする関数をこの後書いていきます。
def validate_email(): return
その後、以下のようなテストコードを書いていきます。今回の要件に沿ったテストコードです。
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
と出力されており、引数がないことがわかります。
このように、「開発・テスト・エラーを確認」というサイクルを回していきます。
最終的に、以下のようなコードが完成しました。
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の記載
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
参考文献
最後に
いかがだったでしょうか?
この記事を通して、少しでもあなたの困りごとが解決したら嬉しいです^^
おまけ(お知らせ)
エンジニアの仲間(データサイエンティストも含む)を増やしたいため、公式LINEを始めました🎉
「一緒に仕事をしてくれる方」「友だちとして仲良くしてくれる方」は、友だち追加をしていただけますと嬉しいです!(仲良くなった人たちを集めて、「ボードゲーム会」や「ハッカソン」や「もくもく会」もやりたいなと考えています😆)
とはいえ、みなさんにもメリットがないと申し訳ないので、特典を用意しました!
友だち追加後に、アンケートに回答してくれた方へ「エンジニア図鑑(職種20選)」のPDFをお送りします◎