Python

【Python】pytestの使い方【テスト駆動開発】

投稿日:

今回は、テスト駆動開発の足掛けとして、pytestの使い方を紹介します。

テスト駆動開発とは、テストのことを第一に考えて実装する開発法のことです。
アジャイル開発のように、設計・開発・テストを短く繰り返す場合に、有効な開発手法とされています。

pytestは、Pythonをテスト実行するためのライブラリです。
入力値と期待する実行結果を記述し、正しく実行完了できるかどうかを検証することができます。

そのため、pytestを先に定義することで、pytestが正しく完了できるような実装をすることで、テスト駆動開発が成立します。

もちろん、開発が終わった後の、単体テスト・結合テストとしてpytestを作成することもできます。

前置きが長くなってしまいましたが、
そんなわけでpytestの使い方を学んで、手戻りのないオンスケの開発をしましょう!

ちなみに、Pythonでテスト駆動開発をする場合は、pytest以外にもunittestライブラリを使用した方法があります。

インストール

pytestのインストール方法です。
PyPlにあります通り、pipコマンドでインストールが可能です。

$ pip install pytest

anacondaをお使いの方は、anacondaでもパッケージが配布されています。
condaコマンドでインストール可能です。

$ conda install -c anaconda pytest

pytestの定義

pytestの書き方を紹介します。

その前に、今回は以下のPythonファイルに記述された関数をテストすることにします。

class Human:
    def __init__(self, name):
        self.name = name

    def call(self):
        str = 'My name is ' + self.name
        return str

Humanクラスに、コンストラクタと3つのメソッドがあります。

これら全てのメソッドを定義するpytestを作成したいと思います。
結論、以下のようになります。

from main import Human

def test_init():
    human = Human('Tanaka')
    assert human.name == 'Tanaka'

def test_type_call():
    human = Human('Yamada')
    assert type(human.call()) == str

def test_var_call():
    human = Human('Sato')
    assert human.call() == 'My name is Sato'

では、こちらのpytestのコードを解説していきます。

pytestでは、関数名の先頭に”test_”と書かれたもののみが実行される関数となります。
”test_”をつけ忘れると、pytestを実行しても、実行の対象とならないため、注意してください。

この”test_”と名づけれた関数を1つのテストとみなします。
(なので、今回の場合は3つテストがあります。)

では、次に関数の中に注目したいと思います。

関数の中は、通常のPythonのコードと同様に、どのような処理を行うか記述します。
test_init関数の例だと、Humanクラスをインスタンス化しています。

関数の最後に、assert句があります。
assert句は、if文やtry文と同様な記法で記述します。

assert句には期待する結果を記載します。
assert句の結果=テストの結果という認識でOKです。

test_init関数の場合は、Humanクラスのnameに’Tanaka’が設定されているか、
test_type_call関数の場合は、call関数の返り値の型がStringか、
test_var_call関数の場合は、call関数の返り値が、’My name is Sato’か、
これらがテストの対象となります。

pytestの実行

では、pytestを実行してみましょう。

pytestの実行は、通常のPythonの実行とは異なり、pytestコマンドでPythonファイルを実行します。

$ pytest unit_test.py

実行結果は、以下になります。

$ pytest unit_test.py 
================================= test session starts ==================================
platform darwin -- Python 3.8.8, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /Users/ユーザー名/test/pytest
collected 3 items                                                                      

unit_test.py ...                                                                 [100%]

================================== 3 passed in 0.01s ===================================

このようになりました。
collected 3 item と記載されている通り、3つのtest関数の実行結果は、collectということになります。

最後の行にも、3 passed in 0.01s と表示されています。
関数の実行が0.01秒だったこともわかります。

では、次にテストに失敗してみましょう。

test_type_call関数のassert句を、以下のように変更してみました。

    assert type(human.call()) == int

human.call()の返り値はString型なので、このテストは失敗になるはずですよね。
では、先程と同じpytestコマンドで実行してみます。

$ pytest unit_test.py 
================================= test session starts ==================================
platform darwin -- Python 3.8.8, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /Users/ユーザー名/test/pytest
collected 3 items                                                                      

unit_test.py .F.                                                                 [100%]

======================================= FAILURES =======================================
____________________________________ test_type_call ____________________________________

    def test_type_call():
        human = Human('Yamada')
>       assert type(human.call()) == int
E       AssertionError: assert <class 'str'> == int
E        +  where <class 'str'> = type('My name is Yamada')
E        +    where 'My name is Yamada' = <bound method Human.call of <main.Human object at 0x7fd88d3e7df0>>()
E        +      where <bound method Human.call of <main.Human object at 0x7fd88d3e7df0>> = <main.Human object at 0x7fd88d3e7df0>.call

unit_test.py:9: AssertionError
=============================== short test summary info ================================
FAILED unit_test.py::test_type_call - AssertionError: assert <class 'str'> == int
============================= 1 failed, 2 passed in 0.07s ==============================

予想通り、Failureとなりました。

.F. と記載されている通り、上から2番目のtest関数の実行が失敗しました。
FAILURESの中にも、どこでエラーが出たのか、何がだめだったのかが表示されていますね。

このようにして、pytestを実行します。

補足

ここまでは基本的な使い方でしたが、ここからはより実践的な使い方を紹介したいと思います。

ディレクトリを分ける

上述の紹介では、main.pyとunit_test.pyが同じ階層にありました。

ですが、同階層にファイルをおいていると、見栄えが良くないですし管理もしにくいです。
パッケージ化するならなおのこと、pytestのファイルは切り分けておいた方が良いです。

というわけで、ディレクトリの分け方と、その際のもう一つの実行方法を紹介します。

ディレクトリの分け方は、以下の通りです。

|- src
|  |- __init__.py
|  |- main.py
|
|- unit_test
|  |- __init__.py
|  |- unit_test.py

Pythonファイル自体は、先ほどと同じmain.pyとunit_test.pyですが、それをディレクトリで分けました。
そして、__init__.pyを作成しました。
__init__.pyを作らないとimportエラーが出るので、作るようにしてください。

__init__.pyの中身は、いずれも空でOKです。

unit_test.pyが参照するmain.pyの場所が変わったので、最初のimport文を以下のように変更します。

from src.main import Human

このimport文ですが、srcとunit_testの親ディレクトリで実行することを想定したものになっています。
変更がある場合は、適宜書き換えてください。

では、実行してみましょう。

$ ls
src             unit_test
$ pytest unit_test/unit_test.py 
================================= test session starts ==================================
platform darwin -- Python 3.8.8, pytest-6.2.3, py-1.10.0, pluggy-0.13.1
rootdir: /Users/ユーザー名/test/pytest
collected 3 items                                                                      

unit_test/unit_test.py ...                                                       [100%]

================================== 3 passed in 0.02s ===================================

このように、先ほどと同様にテストが実行されました。

そのほか、
テスト時間を計測したり、処理を分けたい場合は、こちらを、
CircleCIと連携したい場合は、こちらを見てみてください。
とても参考になります。

ここまで読んでいただき、ありがとうございました。

-Python
-, ,

執筆者:


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

関連記事

【Kivy】Widgetの初期値をidで指定する方法【Python】

Kivyを使ったアプリ開発をやっていると、widgetの初期値について、kvファイルで指定するのではなくpythonファイルで設定したくなることがあると思います。そんな時、root widgetクラス …

【Kivy】ボタンのフィードバックと重い処理を分ける方法【Python】

Kivyでアプリ開発をしていると、MVCのように処理と表示を分けたくなることがあると思います。その時、ボタンをトリガーにすると、ボタンのフィードバックのタイミングが処理の終了タイミングと同じになってし …

【Python】loggingの正しい使い方

開発効率やメンテナンス性の観点から、ログを残すことは非常に重要です。 Pythonにはログ出力用に「logging」モジュールが用意されています。loggingを使うことで、簡単にログ管理をすることが …

【Python】グローバル変数が更新されない時の対処法

Pythonでグローバル変数を取り扱っているとき、関数内でグローバル変数の値を更新しても、更新が反映されていないということはないでしょうか。 これには、Pythonのお作法が関わっています。この解決法 …

【入門】Pythonを学習するうえでやってはいけないこと【アンチパターン】

今回は、Pythonを学習するうえでやってはいけないこと(アンチパターン)を紹介します。 そもそもプログラミング自体が初めてという方も、CやJavaでプログラミングの基礎は習得済という方にも、両方意識 …

yassanさんのプロフィール写真

yassan
R&Dエンジニア。
新卒で大手の社内SEになったもののスキルアップのためにITベンダーに転職。
技術調査からプログラミングまで幅広くやってます。
趣味はバイクとガジェットと遊戯王カードです。