LoginSignup
2
5

More than 5 years have passed since last update.

Pythonで書きたい! (2) テストを書いてみよう

Last updated at Posted at 2017-08-06

こんにちは。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, FalseIntとして評価でき、それぞれ1,0となるからです。

>>> int(True)
1
>>> int(False)
0

今後、型を厳しくチェックする実装を追加すると、本テストが通らなくなる可能性があります。

ですが、True,Falseが指定されるケースは意図したパターンではないはずです。
そのため、今回はTypeErrorを期待するテストを書いておくことにしましょう。
pytest.raiseswith句と合わせて使うことで、例外を期待するテストを書くことができます。

    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で書きたい!です。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5