CLOVER🍀

That was when it all began.

unittestラむブラリで、Pythonのテストコヌドを曞いお実行する

これは、なにをしたくお曞いたもの

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などもあるようなので、
ドキュメントを参照するずよいでしょう。

class unittest.TestCase

クラスずモゞュヌルのフィクスチャ

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の説明を読んでみたす。

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を、実行方法の点からちょっず芋おみたした。

アサヌションや、その他の機胜に぀いおはあたり芋れおいたせんが、ずりあえず初歩的な䜿い方ずしおはわかった感じかなず。