<この記事の著者>
QAエンジニア
文系未経験から開発エンジニアとしてSIerに新卒入社する。その後Web系事業会社も経て開発経験を積んだのち、得意なシステムテスト技術をより活かせるQAエンジニアにジョブチェンジした。
現在は「テストが得意なエンジニア」として上流工程からのテスト活動を推進している。
単体テスト(ユニットテスト、コンポーネントテストとも)はソフトウェア開発プロセスにおけるテスト工程で最初に行われるもので、品質の作り込みにおいて非常に重要なものであることは言うまでもありません。ですが、いざ「単体テストコードを書こう」と思ったときに、どう書けばいいのかぴんとこないこともあります。少なくともかつての私はそうでした。
今回は同じような悩みを持っている方にオススメな単体テストのパターン「3Aパターン」をご紹介します。

【目次】
3Aパターンとは
3Aパターンとは、簡潔に言ってしまえばArrange(準備)、Act(実行)、Assert(検証)の3つのフェーズに分けてテストコードを記述することです。それだけでは何のことだかわからないと思うので、実際のコードを見ていきます。
まずは、テストを行うには当然テストする対象のコードが必要です。今回は「2つの数字を渡すとその商を出力する」というごくシンプルな機能にします。この場合、ゼロ除算の考慮が必要になってきます。
class Calculator: def divide(self, a, b): """2つの数値の商を返す。bが0の場合はZeroDivisionErrorを投げる。""" if b == 0: raise ZeroDivisionError("ゼロ除算エラー") return a / b
このコードをテストするコードは以下のようになります。
import unittest class TestCalculator(unittest.TestCase): def test_divide_normal(self): # Arrange: Calculator オブジェクトの作成とテスト用の入力値を設定 calc = Calculator() a, b = 10, 2 expected_result = 5.0 # Act: divide メソッドを呼び出し result = calc.divide(a, b) # Assert: 結果を検証 self.assertEqual(result, expected_result, "通常の除算の結果が正しくありません") def test_divide_by_zero(self): # Arrange calc = Calculator() a, b = 10, 0 expected_result = None # Act with self.assertRaises(ZeroDivisionError) as context: calc.divide(a, b) # Assert self.assertEqual(str(context.exception), "ゼロ除算エラー") if __name__ == '__main__': unittest.main()
Arrange(準備)
# Arrange: Calculator オブジェクトの作成とテスト用の入力値を設定 calc = Calculator() a, b = 10, 2 expected_result = 5.0
または
# Arrange calc = Calculator() a, b = 10, 0 expected_result = None
この部分のことです。ここでは、以下のようなことを行います。
- オブジェクトの初期化
テスト対象となるクラスやコンポーネントのインスタンスを作成します。今回で言えば、Calculatorクラスがそれに当たります。
- 依存関係の設定
依存している他のクラスやサービスがあれば、それらを適切に設定またはモック(模擬オブジェクト)を使用して準備します。たとえば、データベースへのアクセスを必要とするクラスのテストでは、データベースの代わりにモックオブジェクトを用意して、期待するデータが返されるように設定します。
- テストデータの準備
テストを実行する際に必要となるデータを準備します。これには、データベースに挿入するレコード、API呼び出しに使用するパラメータ、またはファイルの内容などが含まれます。
- 環境設定
テストが特定の環境設定を要求する場合(たとえば、特定の構成ファイルを読み込む、環境変数を設定するなど)、設定を行います。
このフェーズではテスト全般の基盤を作成するため、ここでの準備が不十分だとテストの品質に直接影響します。
Act (実行)
# Act: divide メソッドを呼び出し
result = calc.divide(a, b)
または
# Act with self.assertRaises(ZeroDivisionError) as context: result = calc.divide(a, b)
この部分のことです。ここでは、以下のようなことを行います。
- 実行パラメータの設定
Arrange(準備)で準備した値を元に、実行パラメータを設定します。
- メソッドや関数の呼び出し、プロセスのトリガー、イベントの発生
設定したパラメータを元に、メソッドや関数の呼び出しを行います。メソッドや関数に限らず、バッチなどのプロセスのトリガーを行うこともあれば、GUIのイベント(ボタンのクリックなど)を行う場合もあります。
- 結果の収集
実際に実行した結果を収集します。この値は、Assert(検証)の項目で用います。
Assert (検証)
# Assert: 結果を検証 self.assertEqual(result, expected_result, "通常の除算の結果が正しくありません")
または
# Assert self.assertEqual(str(context.exception), "ゼロ除算エラー")
のことです。ここでは、以下のようなことを行います。
- 結果の検証
テストした結果が期待通りであるかを確認します。今回の場合、「通常の計算処理」と「ゼロ除算を行った場合」でシナリオを分けているため、期待値もそれぞれ異なります。
あるオブジェクトが特定の状態にあるか、例外が適切に発生しているかなども検証します。
- 副作用の検証
コードの実行が期待される副作用を引き起こしたかどうかを確認します。ログファイルへの書き込み、外部システムとの通信、APIからの応答など。
コードを実行しただけでは当然正しく動作したとは言えないので、期待する結果と適切に突き合わせて期待通りの動作をしているかを期待値ごとに確認する必要があります。
3Aパターンのメリットとデメリット
ここからは、3Aパターンのメリットとデメリットについて解説します。
メリット
構造がわかりやすい
どこに何が書いてあるかをパターン化することによって、他の開発者がテストコードを読んだり、保守したりする際に理解しやすくなります。
修正の効率化
Actで発生した問題は、具体的な操作や関数呼び出しに関連しているため、修正がしやすくなります。Assertでの失敗は、期待値との不一致によるものなので、問題の特定が簡単になります。
デメリット
逆にわかりづらくなることがある
全てのテストケースを3Aの形式に無理に当てはめようとすると、逆にわかりづらくなることがあります。
同じ処理を何度も繰り返す場合がある
共通の準備が必要な場合、それぞれのテストで同じArrangeコードを繰り返すことになり、冗長になります。この場合は、Arrangeを適宜別のメソッドにまとめる必要があります。
必ずしも3Aパターンに当てはめる必要があるというわけではなく、状況に応じて適切な書き方を検討する必要があります。
終わりに
もしコードが複雑過ぎてパターンに当てはめることも難しい、という場合は、そのテストしたいコンポーネントが本来何をするものなのかを整理した上で、テストしやすい形に切り分ける(=リファクタする)のが、品質向上の第一歩になります。
3Aパターンに当てはめれば全て正解というわけでは必ずしもありませんが、これから単体テストに初めて手をつける、という方の参考になれば幸いです。
(文:かいり)

「paizaラーニング」では、未経験者でもブラウザさえあれば、今すぐプログラミングの基礎が動画で学べるレッスンを多数公開しております。
詳しくはこちら
