これは、なにをしたくて書いたもの?
Pythonを勉強するにあたって、テストコードまわりについて少し押さえておいた方がいいかなぁと思いまして。
Pythonには、いくつかテストをサポートするライブラリ、ツールがあるようです。
コードのテスト — The Hitchhiker's Guide to Python
Testing Your Code — The Hitchhiker's Guide to Python
今回は基本である、unittestを使ってみることにしました。
unittest
Python標準に含まれる、JUnitに触発されたテスティングフレームワークです。
26.4. unittest --- ユニットテストフレームワーク — Python 3.6.9 ドキュメント
unittest ユニットテストフレームワークは元々 JUnit に触発されたもので、 他の言語の主要なユニットテストフレームワークと同じような感じです。 テストの自動化、テスト用のセットアップやシャットダウンのコードの共有、テストのコレクション化、そして報告フレームワークからのテストの独立性をサポートしています。
テストケースの作成のためのクラスやアサーションを含み、ランナーも提供します。
アサーションについては、こちら。
assertAlmostEqual
※assertAlmostEqualメソッドのリファレンスの上に、一覧があります
実行については、unittestをコマンドラインからモジュール指定で実行することで行います。
特定のディレクトリの配下のテストコードを検出する、テストディスカバリも可能です。
ところで、コードのテストでも紹介されていますが、pytestというものも押さえておいた方がよさそうなので、こちらもそのうち。
pytest: helps you write better programs — pytest documentation
環境
今回の環境は、こちらです。
$ python3 -V Python 3.6.8
お題とプロジェクト構成
テスト対象のコード(アプリケーションコード)と、テストコードを同じディレクトリに配置して、サンプル的に動かしてみても
いいのですが、せっかくなら実際に使う時の構成を意識してみたいなぁと思います。
テストコードを配置するディレクトリ構成は、pytestのドキュメントを参考に。
Choosing a test layout / import rules
テストコードをアプリケーションコードの外に置くスタイルと
Tests outside application code
テストコードをアプリケーションコードの中に置くスタイルがあるようです。
Tests as part of application code
今回は、テストコードをアプリケーションコードの外に置くことにしました。
こんなディレクトリ構成にします。
sample ## ← アプリケーションコードを置く tests ## ← テストコードを置く
テストコードのディスカバリも試すために、アプリケーションコードを2つのファイルで作成し、対応するテストも2つ用意する構成に
したいと思います。
テストを作成して、実行してみる
まず最初に、アプリケーションコードを書きます。 sample/calc.py
class Calc: def add(self, x, y): return x + y def minus(self, x, y): return x - y def multiply(self, x, y): return x * y def divide(self, x, y): return x / y
これに対応する、テストコードを書きましょう。
unittestを使う場合、テストはTestCaseクラスのサブクラスとして作成するようです。
また、テストを行うメソッド名は、「test」で始まる必要があるようです。
テストケースは、 unittest.TestCase のサブクラスとして作成します。メソッド名が test で始まる三つのメソッドがテストです。テストランナーはこの命名規約によってテストを行うメソッドを検索します。
アサーションは、unittest(というかTestCaseクラス)が提供するメソッドを使用して行います。
で、作成したのがこちら。
tests/test_calc.py
import unittest from sample.calc import Calc class CalcTestCase(unittest.TestCase): def setUp(self): print("setUp!!") def tearDown(self): print("tearDown!!") def test_add(self): sut = Calc() self.assertEqual(sut.add(1, 3), 4) def test_minus(self): sut = Calc() self.assertEqual(sut.minus(5, 3), 2) def test_multiply(self): sut = Calc() self.assertEqual(sut.multiply(2, 3), 6) def test_divide(self): sut = Calc() self.assertEqual(sut.divide(10, 2), 5) def foo(self): print("foo!!")
しれっと、「test」で始まらないメソッドも含めてあります。
テストメソッドごとに実行するsetUpやtearDownも書いてみました。クラス単位のsetUpClass、tearDownClassなどもあるようなので、
ドキュメントを参照するとよいでしょう。
テストを実行してみます。
$ python3 -m unittest tests.test_calc setUp!! tearDown!! .setUp!! tearDown!! .setUp!! tearDown!! .setUp!! tearDown!! . ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
setUpやtearDownが、テストメソッドごとに動いているような感じがします。
が、詳細がわからないので「-v」を付けてみます。
$ python3 -m unittest tests.test_calc -v test_add (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_divide (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_minus (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_multiply (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK
なるほど、これだと実行されたテストメソッドもわかりますね。
以下のメソッドが対象に含まれていないことも確認できました。
def foo(self): print("foo!!")
また、テストに失敗するようなコードになっている場合は、こんな表示になります。
======================================================================
FAIL: test_add (tests.test_calc.CalcTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/path/to/tests/test_calc.py", line 14, in test_add
self.assertEqual(sut.add(1, 3), 5)
AssertionError: 4 != 5
あともうひとつ、アプリケーションコードを追加して
sample/message.py
class Decorator: def decorate(self, message, character): return "{0}{1}{2}".format(character, message, character)
テストも足しておきましょう。
tests/test_message.py
import unittest from sample.message import Decorator class DecoratorTestCase(unittest.TestCase): def test_decorate(self): sut = Decorator() self.assertEqual(sut.decorate("Hello World!!", "***"), "***Hello World!!***")
確認。
$ python3 -m unittest tests.test_message -v test_decorate (tests.test_message.DecoratorTestCase) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
なお、テスト対象を複数指定して実行することもできます。
$ python3 -m unittest tests.test_calc tests.test_message -v test_add (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_divide (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_minus (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_multiply (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_decorate (tests.test_message.DecoratorTestCase) ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.001s OK
unittest#main
ドキュメントの以下の部分にも書いているのですが、
基本的な使い方だと、unittest#mainを呼び出すように、テストコードに書くようです。
if __name__ == '__main__': unittest.main()
で、Pythonコマンドで直接実行する、と。
$ python3 test_example.py
これでも良いのですが、今回のようにアプリケーションコードとテストコードを別々にする方法だと、モジュールのパス解決で
困ったことになったので、今回はパス…。
テストディスカバリを行う
ここまでは、テストコードをひとつひとつ指定して実行してきましたが、テストディスカバリを使うとテストコードを見つけて
くれるようです。
シンプルな実行方法は、以下だとか。
$ python3 -m unittest ### または $ python3 -m unittest discover
試してみます。
$ python3 -m unittest ---------------------------------------------------------------------- Ran 0 tests in 0.000s OK
…テストが実行されなかったようです。
こここで、TestLoader#discoverの説明を読んでみます。
指定された開始ディレクトリからサブディレクトリに再帰することですべてのテストモジュールを検索し、それらを含む TestSuite オブジェクトを返します。pattern にマッチしたテストファイルだけがロードの対象になります。
モジュールについて、もうちょっと調べてみます。
あるディレクトリを、パッケージが入ったディレクトリとしてPython に扱わせるには、ファイル __init__.py が必要です。
どうやら、__init__.pyが必要な雰囲気があります。
作成。
$ touch tests/__init__.py
再度、実行。
$ python3 -m unittest setUp!! tearDown!! .setUp!! tearDown!! .setUp!! tearDown!! .setUp!! tearDown!! .. ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
今度は動きましたね。
「-v」オプションを指定してみましょう。
$ python3 -m unittest -v test_add (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_divide (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_minus (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_multiply (tests.test_calc.CalcTestCase) ... setUp!! tearDown!! ok test_decorate (tests.test_message.DecoratorTestCase) ... ok ---------------------------------------------------------------------- Ran 5 tests in 0.001s OK
また、ディスカバリを開始するディレクトリを「-s」オプションで指定したり、テストが書かれたファイル名のパターンを「-p」で
指定したりできますが、これらを使う場合は「discover」サブコマンドの指定が必須になります。
「discover」サブコマンドの指定なしで、「-p」オプションを指定するとエラーになりますが
$ python3 -m unittest -p 'test_*.py'
usage: python3 -m unittest [-h] [-v] [-q] [--locals] [-f] [-c] [-b]
[tests [tests ...]]
python3 -m unittest: error: unrecognized arguments: -p
「discover」サブコマンドを指定すると、動作します。
$ python3 -m unittest discover -p 'test_*.py' setUp!! tearDown!! .setUp!! tearDown!! .setUp!! tearDown!! .setUp!! tearDown!! .. ---------------------------------------------------------------------- Ran 5 tests in 0.000s OK
まとめ
Pythonの標準テストライブラリであるunittestを、実行方法の点からちょっと見てみました。
アサーションや、その他の機能についてはあまり見れていませんが、とりあえず初歩的な使い方としてはわかった感じかなと。