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を、実行方法の点からちょっと見てみました。

アサーションや、その他の機能についてはあまり見れていませんが、とりあえず初歩的な使い方としてはわかった感じかなと。