Pythonプログラムのテストを実行するためのテストフレームワークであるunittestモジュールについて解説します。
Contents
unittestとは
unittestは、Python標準のテストフレームワークです。unittestの公式ドキュメントはこちらを参照してください。
unittestは公式ドキュメントにも記載がありますが、もともとはJavaのJUnitに触発されて作られているものなので、JavaでJUnit等のテストフレームワークを使用したことがある人は似たような感覚で使い始められるかと思います。
テスト駆動開発(TDD:Test-Driven Development)という自動テストを中心にした高品質のソフトウェア開発をするための方法論があり、Pythonコミュニティでは幅広く利用されています。テスト駆動開発においては、unittestのようなテストフレームワークは非常に重要なものです。テスト駆動開発については「エキスパートPythonプログラミング」という書籍にも記載がありますので、興味があれば参考にしてもらえるとよいかと思います。
本記事では、Pythonの標準テストフレームワークであるunittestを使用したテストの基本的な使い方について紹介していきます。
Pythonのテストフレームワークは他にもあります。色々とあるテストフレームワークの中でもpytestというテストフレームワークは非常におすすめなテストフレームワークです。pytestの使い方については「Pythonプログラムをpytestでテストする方法」にまとめていますので興味があれば参考にしてください。
unittestの使い方
プログラムのテストのイメージに慣れていない方もいるかもしれませんので、まずは以下の図を使って簡単なテストイメージを紹介します。以降で具体的なプログラムも使って説明をしていきますので、ここではまずイメージをつかんでもらえればと思います。
sample.pyというプログラムを作成しテストしたいと考えているとします。このsample.pyにはSampleというクラスがあり、xとyを足して2倍にするだけのシンプルなadd_and_doubleというメソッドが定義されているものとします。
このプログラムをテストしたい場合には「test_sample.py」といったテストプログラムを作成します。unittestではtestという名前から始まるファイルにしておくと探索してまとめて実行するようなことも可能です。テストプログラム内では、sampleをインポートして、unittest.TestCaseを継承してTestSampleのようなクラスを作成します。
unittestでは、TestCaseを継承したクラス内で「test」で始まるメソッドをテストとして実行します。なお、unittestはunittest.main()で実行します。
各テストプログラム内では「self.assertEqual(self.temp.add_and_double(1, 1), 4)」のようにテスト対象モジュールの返却値などに対する判定を「assertXXXX」といった形のunittest.TestCaseで定義されているメソッドを使って記述します。この判定式の結果が一致しない場合にはテスト失敗となるわけです。
なお、assertXXXXについては、一致を確認するassertEqual、真(True)かを判定するassertTrueなど各種用意されています。各種メソッドの詳細はunittest.TestCaseの公式ドキュメントのこちらを確認してください。
上記例では、x=1, y=1として引数を渡していますので、(1+1)×2=4をadd_and_doubleメソッドは返却してほしいわけですが、異なる値が返ってくるということはメソッドの処理に何か問題があるということになります。なお、assertXXXXは一つのメソッドに複数行書いて問題ありません。
テストプログラム内にて、テスト観点毎にテストを実行するメソッドをどんどん追加していくわけです。unittestを実行するとテストプログラム内に定義されているテスト用メソッドが順番に実行されていき、それぞれが成功したか失敗したかを出力してくれます。
まずはテストに関して上記のようなイメージを持ってもらえればと思います。では、以降ではunittestの実行方法や具体的な例を使った説明に進んでいきたいと思います。
unittestの実行方法
unittestを実行する場合には、テストプログラム内に以下のような記述を入れておいてください。unittest.main()がテストを実行してくれます。
if __name__ == "__main__": # unittestを実行する unittest.main()
上記の記載があるテストプログラムを実行すると、unittest.TestCaseを継承したクラスのtestから始まるメソッドをテスト対象としてunittestによるテストが実行されます。なお、「-v」を入れておくと詳細情報が表示されます。
python test_sample.py -v
なお、通常は複数のテストプログラムをまとめて実行したくなるかと思います。unittestは、以下のようなコマンドを使用することで、testから始まる名前のファイルをテスト対象として判断してくれるため該当フォルダ内を探索してtestから始まる名前のファイルを順番にテストしてくれます。
python -m unittest -v
unittestによるテストの実装例
ここからは、より具体的にunittestを使用したテスト実装例を見ていきましょう。まずは、以降で説明する関連プログラムをまとめて示します。
テスト対象のsample.py、テストプログラムのtest_sample.pyがあります。詳細内容は後ほど順番に説明していきます。
sample.py:テスト対象プログラム
class Sample: """Sampleクラス""" def add_and_double(self, x, y): """xとyを足して2倍した値を返却する Args: x: 入力値1 y: 入力値2 """ # intでない場合は、ValueErrorとする if not isinstance(x, int) or not isinstance(y, int): raise ValueError # 計算処理 result = x + y result *= 2 return result
test_sample.py:テストプログラム
import unittest import sample # スキップ用フラグ SKIP_FLAG = True class TestSample(unittest.TestCase): """Sampleクラスのテスト用クラス""" @classmethod def setUpClass(cls): print("\nStart TestSample") cls.temp = sample.Sample() @classmethod def tearDownClass(cls): print("\nEnd of TestSample") del cls.temp def setUp(self): print("\nTest Start") def tearDown(self): print("End of Test") def test_add_and_double(self): """テストケース1: 正常計算""" self.assertEqual(self.temp.add_and_double(1, 1), 4) self.assertEqual(self.temp.add_and_double(2, 2), 8) # self.assertEqual(self.temp.add_and_double(2, 2), 4) def test_add_and_double_raise(self): """テストケース2: 例外処理""" with self.assertRaises(ValueError): self.temp.add_and_double("1", 1) with self.assertRaises(ValueError): self.temp.add_and_double(1, "1") with self.assertRaises(ValueError): self.temp.add_and_double("1", "1") # with self.assertRaises(ValueError): # self.temp.add_and_double(1, 1) @unittest.skip("skip理由:xxxxxxxxxx") def test_add_and_double_skip(self): """スキップ確認用""" assert self.temp.add_and_double(1, 1) == 4 @unittest.skipIf(SKIP_FLAG is True, "条件付きskip理由:xxxxxxxxx") def test_add_and_double_skipif(self): """条件付きスキップ確認用""" assert self.temp.add_and_double(1, 1) == 4 if __name__ == "__main__": # unittestを実行する unittest.main()
実行結果 「python -m unittest -v」または「python test_sample.py -v」を実行
> python -m unittest -v Start TestSample test_add_and_double (test_sample.TestSample) テストケース1: 正常計算 ... Test Start End of Test ok test_add_and_double_raise (test_sample.TestSample) テストケース2: 例外処理 ... Test Start End of Test ok test_add_and_double_skip (test_sample.TestSample) スキップ確認用 ... skipped 'skip理由:xxxxxxxxxx' test_add_and_double_skipif (test_sample.TestSample) 条件付きスキップ確認用 ... skipped '条件付きskip理由:xxxxxxxxx' End of TestSample ---------------------------------------------------------------------- Ran 4 tests in 0.004s OK (skipped=2)
以降では上記プログラムを使ってポイントとなるテスト記載方法を説明していきます。
テストの基本要素
テストの基本構成についてまず説明します。以下は、test_sample.py内のTestSampleクラスの一部抜粋です。
class TestSample(unittest.TestCase): """Sampleクラスのテスト用クラス""" def test_add_and_double(self): """テストケース1: 正常計算""" # print("実行:test_add_and_double") self.assertEqual(self.temp.add_and_double(1, 1), 4) self.assertEqual(self.temp.add_and_double(2, 2), 8)
unittestを使用したテストでは、上記のTestSampleクラスのようにunittest.TestCaseを継承したテストクラスを作成し、その中にテスト用メソッドをどんどん記載していきます。
test_add_and_double(self)は、正常計算結果を確認することを意図して作成したテストメソッドです。
テスト結果の判定を行う場合には、上記のassertEqualのようにunittestに実装されているテスト判定用のメソッドを使用します。第1引数は判定対象のメソッドの返り値で、第2引数が正解として比較する値を指定しています。この判定が問題なく処理されれば、テストはOKとなります。
他にも判定用メソッドは色々ありますので、各種メソッドの詳細はunittest.TestCaseの公式ドキュメントのこちらを確認してください。
上記で記載しているケースは問題なく処理がされるため、実行結果を見ると以下のようにokとなります。
test_add_and_double (test_sample.TestSample) テストケース1: 正常計算 ... ok
では、失敗する状況を考えてみます。例えば「self.assertEqual(self.temp.add_and_double(2, 2), 4)」のようなテストをした場合には、以下のように失敗となります。なお、これは(2+2)×2=8ですので、テストケースが間違っている例です。
====================================================================== FAIL: test_add_and_double (test_sample.TestSample) テストケース1: 正常計算 ---------------------------------------------------------------------- Traceback (most recent call last): File "D:\PythonProjects\python-tech-sample-source\python-tech-sample-source\python-libraries\unittest\test_sample.py", line 39, in test_add_and_double self.assertEqual(self.temp.add_and_double(2, 2), 4) AssertionError: 8 != 4
このように想定したものと出力結果が異なる場合にはFAILとして失敗が表示されます。
なお、テストの失敗としては「テスト対象プログラムの処理結果が誤っている場合」や上記のように「テストケース自体が誤っているような場合」等、色々ありますのでエラーを見つつ、プログラムを修正していくようにしてください。
例外処理
次に、例外処理をテストする場合のテストメソッドの記載方法です。
def test_add_and_double_raise(self): """テストケース2: 例外処理""" with self.assertRaises(ValueError): self.temp.add_and_double("1", 1) with self.assertRaises(ValueError): self.temp.add_and_double(1, "1") with self.assertRaises(ValueError): self.temp.add_and_double("1", "1")
今回のテスト対象プログラムsample.pyのadd_and_doubleメソッドでは、intではない引数が渡された場合にValueErrorを返却することになっています。
このような場合には、上記のようにself.assertRaisesを使用したwith句を用いて確認します。もし「self.temp.add_and_double(1, 1)」のように例外をあげないような処理をwith句内に記載するとテストは失敗となります。
setUpとtearDown
次に、各テストの実行前後にテストのセットアップや完了時処理をするためのsetUpとtearDownについての記載方法です。
@classmethod def setUpClass(cls): print("\nStart TestSample") cls.temp = sample.Sample() @classmethod def tearDownClass(cls): print("\nEnd of TestSample") del cls.temp def setUp(self): print("\nTest Start") def tearDown(self): print("End of Test")
setUpは、テストの前に準備をするために用意されているものでsetUpClassやsetUpがあります。クラス生成時のセットアップがsetUpClassで、メソッドの実行時のセットアップがsetUpです。
一方で、tearDownは、テストの後に処理を実行するために用意されているもので、tearDownClassやtearDownがあります。それぞれクラスとメソッドに対応します。
上記例では、テスト開始と終了、各メソッドの開始と終了が分かるようにprintしています。実行結果を見ると以下のように表示されます
> python -m unittest -v Start TestSample test_add_and_double (test_sample.TestSample) テストケース1: 正常計算 ... Test Start End of Test ok test_add_and_double_raise (test_sample.TestSample) テストケース2: 例外処理 ... Test Start End of Test ok test_add_and_double_skip (test_sample.TestSample) スキップ確認用 ... skipped 'skip理由:xxxxxxxxxx' test_add_and_double_skipif (test_sample.TestSample) 条件付きスキップ確認用 ... skipped '条件付きskip理由:xxxxxxxxx' End of TestSample ---------------------------------------------------------------------- Ran 4 tests in 0.004s OK (skipped=2)
setUpClassとtearDownClassのprintは最初と最後のみ表示されていることが分かります。一方でsetUpとtearDownのprintは各メソッドの実行前後で表示されています。
また、setUpClassではあらかじめテスト対象のクラスSampleをオブジェクト化しており、tearDownClassでは(特に必須なわけではないですが)オブジェクトをdelしています。このようにすることで、各テストメソッドでは毎回クラスをオブジェクト化する必要がなくなりますし、テスト終了時の処理を必要に応じて記載できます。
テスト前後のタイミングで何か処理を記載したい場合には、setUpやtearDownをうまく活用しましょう。
テストのスキップ
テストを多く記載していると一部のテストメソッドをスキップして処理を実行したくなる場合があります。テストをスキップする場合の記載方法について見ていきましょう。
通常のスキップ
一部のテストメソッドをスキップしたい場合には、以下のように@unittest.skipデコレータを使用します。引数にskip理由などを記載しておき実行時に表示することが可能です。
@unittest.skip("skip理由:xxxxxxxxxx") def test_add_and_double_skip(self): """スキップ確認用""" assert self.temp.add_and_double(1, 1) == 4
スキップされると以下のような実行結果となります。
test_add_and_double_skip (test_sample.TestSample) スキップ確認用 ... skipped 'skip理由:xxxxxxxxxx'
条件付きスキップ
条件に一致する場合にテストをスキップしたいときもあるかと思いますが、その場合は以下のように@unittest.skipIfデコレータを使用して条件を引数に渡します。skip理由も引数に渡すことで表示することが可能です。
# スキップ用フラグ SKIP_FLAG = True ...(省略)... @unittest.skipIf(SKIP_FLAG is True, "条件付きskip理由:xxxxxxxxx") def test_add_and_double_skipif(self): """条件付きスキップ確認用""" assert self.temp.add_and_double(1, 1) == 4
スキップされると以下のような実行結果となります。
test_add_and_double_skipif (test_sample.TestSample) 条件付きスキップ確認用 ... skipped '条件付きskip理由:xxxxxxxxx'
まとめ
Pythonプログラムのテストを実行するためのテストフレームワークであるunittestモジュールについて解説しました。unittestはPython標準のテストフレームワークです。
本記事内では、unittestの実行方法やテスト実装例について紹介してきました。setUpやteaDownを使ったテスト準備やテストのスキップについて紹介しています。
unittestは、Python標準のテストフレームワークなので皆さんも色々と使い方を調べてもらうと面白いかと思います。
また、テスト駆動開発(TDD:Test-Driven Development)という自動テストを中心にした高品質のソフトウェア開発をするための方法論はPythonコミュニティでは幅広く利用されています。unittestはテスト駆動開発においても使用できるテストフレームワークです。テスト駆動開発については「エキスパートPythonプログラミング」という書籍にも記載がありますので、興味があれば参考にしてもらえるとよいかと思います。
unittestの公式ドキュメントはこちらを参照してください。
上記で紹介しているソースコードについてはgithubにて公開しています。参考にしていただければと思います。