こんにちは。leo1109です。
今回は前回の記事(コードフォーマットチェック)の続きになります。
記事で利用したコードは、すべてGitHub上にアップロードされています。
今回紹介する話
テストを書く話です。pytestを利用します。
Pythonでテストを書きたい!
pytestのドキュメントには以下の例があります。
inc(x) は、xに1を足した値を返す関数です。(Increment)
テストとして、test_answer()が定義されています。
# content of test_sample.py
def inc(x):
return x + 1
def test_answer():
assert inc(3) == 5
テストを実行します。
pytestのインストールが必要ですので、pipでインストールを実行するか、
リポジトリにあるrequirements.txtを利用してインストールしてください。
pip install pytest
pip install -r requrements.txt
pytestを実行してみます。
$ pytest
======= test session starts ========
collected 1 item
test_sample.py F
======= FAILURES ========
_______ test_answer ________
def test_answer():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_sample.py:5: AssertionError
======= 1 failed in 0.12 seconds ========
inc(3)は、4を返す想定ですから、5と等しくはないため、失敗と判定されました。
get_fibonacci_by_index()をテストする
では、早速テストを書いてみましょう。
get_fibonacci_by_index(1)
は1を返すので、1を返すことを確認するテストを書いてみます。
# python 3.5.2
import my_math
class TestGetFibonacciByIndex:
def test(self):
assert my_math.get_fibonacci_by_index(1) == 1
以下は実行した結果です。
$ pytest my_math_test.py
================================================================================= test session starts =================================================================================
my_math_test.py .
============================================================================== 1 passed in 0.03 seconds ===============================================================================
先ほどとは異なり、1 passed, と表示されました。
他のパターンも合わせてテストしてみましょう。
様々なassert
テストケースを追加するには、そのままassertを追加していく方法が簡単です。
テストの粒度によっては、戻り値がIntであることを確認する必要があるかもしれません。
Pythonのコードとしてかけますので、様々な演算子はもちろん、配列、辞書なども利用できます。
# python 3.5.2
import my_math
class TestGetFibonacciByIndex:
def test(self):
assert my_math.get_fibonacci_by_index(1) == 1
assert my_math.get_fibonacci_by_index(2) == 1
assert my_math.get_fibonacci_by_index(3) == 2
assert my_math.get_fibonacci_by_index(4) > 2
assert (my_math.get_fibonacci_by_index(5) == 5) is True
def test_is_instance(self):
assert isinstance(my_math.get_fibonacci_by_index(2), int)
def test_as_array(self):
expected = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
got = []
for i in range(1, 11):
got.append(my_math.get_fibonacci_by_index(i))
assert expected == got
実行してみましょう。
vオプションをつけて実行すると、テストのメソッドごとに結果が表示されます。
テストが実行されるためには、メソッド名がtestから始まっている必要があります。
$ pytest my_math_test.py -v
================================================================================= test session starts =================================================================================
my_math_test.py::TestGetFibonacciByIndex::test PASSED
my_math_test.py::TestGetFibonacciByIndex::test_is_instance PASSED
my_math_test.py::TestGetFibonacciByIndex::test_as_array PASSED
============================================================================== 3 passed in 0.09 seconds ===============================================================================
意図しない引数でのテスト
そういえば、get_fibonacci_by_index(x)
に0や負の数を与えるとどのような挙動になるのでしょうか。今までは自然数しか与えていませんでしたね。
0や-1を与えてみたところ、1が取得できました。
これは意図した動作になっているでしょうか?
>>> import my_math; my_math.get_fibonacci_by_index(0)
1
>>> import my_math; my_math.get_fibonacci_by_index(-1)
1
ソースコードを見ると、range(1, x)
に入らない限りは1を返すようになっているようです。
様々なケースを考慮してテストを書くことは重要ですが、あまりにも手を広げすぎると大変です。
例えば、引数に文字列が来た場合は?bool値が来た場合は?など、です。
今回のget_fibonacci_by_index(x)
では、(x)は自然数を対象としているはずです。
ですので、自然数以外は想定しない、という定義にしても良いはずです。
...ですが、今回はテストを書いてみよう!という記事ですので、少し寄り道をしてみましょう。
以下のテストケースを見てください。
def test_bool(self):
assert my_math.get_fibonacci_by_index(True) == 1
assert my_math.get_fibonacci_by_index(False) == 1
このテストは想定に反してPassします。
理由は、True, False
はInt
として評価でき、それぞれ1,0となるからです。
>>> int(True)
1
>>> int(False)
0
今後、型を厳しくチェックする実装を追加すると、本テストが通らなくなる可能性があります。
ですが、True,False
が指定されるケースは意図したパターンではないはずです。
そのため、今回はTypeError
を期待するテストを書いておくことにしましょう。
pytest.raises
をwith
句と合わせて使うことで、例外を期待するテストを書くことができます。
def test_bool(self):
with pytest.raises(TypeError):
my_math.get_fibonacci_by_index(True)
with pytest.raises(TypeError):
my_math.get_fibonacci_by_index(False)
ですが、当然テストは通りません。
この状態で放置しておくのは、あまり良くなさそうです...
$ pytest -v my_math_test.py
================================================================================= test session starts =================================================================================
my_math_test.py::TestGetFibonacciByIndex::test PASSED
my_math_test.py::TestGetFibonacciByIndex::test_is_instance PASSED
my_math_test.py::TestGetFibonacciByIndex::test_as_array PASSED
my_math_test.py::TestGetFibonacciByIndex::test_bool FAILED
====================================================================================== FAILURES =======================================================================================
__________________________________________________________________________ TestGetFibonacciByIndex.test_bool __________________________________________________________________________
self = <my_math_test.TestGetFibonacciByIndex object at 0x10566de10>
def test_bool(self):
with pytest.raises(TypeError):
> my_math.get_fibonacci_by_index(True)
E Failed: DID NOT RAISE <class 'TypeError'>
my_math_test.py:30: Failed
========================================================================= 1 failed, 3 passed in 0.12 seconds ==========================================================================
ですので、このテストはいったんスキップしておくことにしましょう。
テストをスキップする
pytestをデコレータとして利用してみます。
pytest.mark.skip
を利用することで、対象のテストをスキップできます。
importにpytest
を追加してください。
import pytest
..
@pytest.mark.skip
def test_bool(self):
with pytest.raises(TypeError):
my_math.get_fibonacci_by_index(True)
with pytest.raises(TypeError):
my_math.get_fibonacci_by_index(False)
スキップする条件を指定することもできます。
sys.version_info
はPythonのバージョンを取得するイディオムです。
もちろん、sysのimportを忘れずに。
import sys
..
@pytest.mark.skipif(sys.version_info < (3,5),
reason="requires python 3.5")
def test_requires_35(self):
assert True
Python 2.7.11と、Python 3.5.2でそれぞれ実行してみると、2.7.11では対象のテストがskipされていることがわかります。
## Python2.7.11
$ pytest -v my_math_test.py
================================================================================= test session starts =================================================================================
platform darwin -- Python 2.7.11, pytest-3.2.0, py-1.4.34, pluggy-0.4.0
my_math_test.py::TestGetFibonacciByIndex::test_requires_35 SKIPPED
## Python3.5.2
$ pytest -v my_math_test.py
================================================================================= test session starts =================================================================================
platform darwin -- Python 3.5.2, pytest-3.2.0, py-1.4.34, pluggy-0.4.0
my_math_test.py::TestGetFibonacciByIndex::test_requires_35 PASSED
特定のバージョンしか実装されていない機能を利用している場合や、複数バージョンに対応するモジュールを実装する場合に便利ですね。
また、実行するPythonのバージョンを切り替える際には、pyenv
が便利です。
(pyenvの説明については本章の内容とは離れますので、割愛させていただきます)
Importの順序を調整する
上記の様にコードを書いていくと、以下の3つのimportが書かれているはずです。
import sys
import pytest
import my_math
実は、この順序も調整するためのツールが用意されています。
isortを利用すると、importの並べ替えを行うことができます。
(他にも様々な機能がありますが、こちらは簡単な紹介のみにとどめておきます。)
さて、無事テストを書くことができました。非常に簡単ですね!
コードを追記した際は、テストを書く癖をつけることで、実装のミスの軽減にも繋がります。
次回は
複雑なメソッドのテストをPythonで書きたい!です。