pytest

【pytest】Pythonプログラムをpytestでテストする方法

【pytest】Pythonプログラムをpytestでテストする方法

Pythonプログラムのテストを実行するためのフレームワークであるpytestモジュールについて解説します。

pytestとは

pytestとは、Pythonのテストフレームワークです。公式サイトはこちらを参照してください。

テスト駆動開発(TDD:Test-Driven Developmentという自動テストを中心にした高品質のソフトウェア開発をするための方法論があり、Pythonコミュニティでは幅広く利用されています。テスト駆動開発においては、pytestのようなテストフレームワークは非常に重要なものです。テスト駆動開発については「エキスパートPythonプログラミング」という書籍にも記載がありますので、興味があれば参考にしてもらえるとよいかと思います。

さて、Python標準のテストフレームワークとしてはunittestモジュールというものがありますが、機能制限などに不満を持つ人たちが出てきました。それらの開発者によって開発されたテストフレームワークの一つがpytestです。他にもテスト用のモジュールはいくつか存在します。

pytestは「Python Software Foundation」と「JetBrains」 が共同で実施しているPython開発者アンケートにおいて「ユニットテストフレームワーク」項目で50%の回答となっていることから広く使われているテストフレームワークであると考えられます。pytestはテストフレームワークとして学ぶ意義があるモジュールであると思っています。

本記事では、pytestを使用したテストの基本的な使い方について紹介していきます。

Python標準のunittestの使い方については「Pythonプログラムをunittestでテストする方法」にまとめていますので興味があれば参考にしてください。

pytestの使い方

プログラムのテストのイメージに慣れていない方もいるかもしれませんので、まずは以下の図を使って簡単なテストイメージを紹介します。以降で具体的なプログラムも使って説明をしていきますので、ここではまずイメージをつかんでもらえればと思います。

pytestのイメージ

sample.pyというプログラムを作成しテストしたいと考えているとします。このsample.pyにはSampleというクラスがあり、xとyを足して2倍にするだけのシンプルなadd_and_doubleというメソッドが定義されているものとします。

このプログラムをテストしたい場合には「test_sample.py」というようなテストプログラムを作成します。テストプログラム内では、sampleをインポートして、sample内のクラスSampleを生成してメソッドadd_and_doubleを呼び出して使用しています。

pytestでは「test」で始まるファイル名をテストプログラムと認識します。また、該当するテストプログラムファイル内で以下に一致するものをテスト対象と判断します。

  • Testから始まるクラス
  • testから始まるモジュール関数

なお、大文字小文字を区別しますので「testから始まるクラス」や「Testから始まる関数」は作らないように注意しましょう。

各テストプログラム内では「assert temp.add_and_double(1, 1) == 4」のようにテスト対象モジュールの返却値などに対する判定を「assert」を使って記述します。この判定式の結果が一致しない場合にはテスト失敗となるわけです。

上記例では、x=1, y=1として引数を渡していますので、(1+1)×2=4をadd_and_doubleメソッドは返却してほしいわけですが、異なる値が返ってくるということはメソッドの処理に何か問題があるということになります。なお、assert文は一つの関数やメソッドに複数行書いて問題ありません。

テストプログラム内にて、テスト観点毎にテストを実行する関数やクラスのメソッドをどんどん追加していくわけです。pytestを実行するとテストプログラム内に定義されているテスト用関数やメソッドが順番に実行されていき、それぞれが成功したか失敗したかを出力してくれます。

まずはテストに関して上記のようなイメージを持ってもらえればと思います。では、以降ではpytestを使用する準備や実行方法、そして具体的な例を使った説明に進んでいきたいと思います。

pytestの使用準備と実行方法

pytestのインストール

pytest使用するにはパッケージのインストールが必要です。以下のコマンドでインストールしてください。完了すればpytestの実行環境が整います。

pip install pytest

pytestの実行方法

pytestを実行するには、テスト対象プログラムが配置されているフォルダにて以下コマンドを実行します。なお、test_sample.pyの部分は例ですので、テスト対象のテストプログラム名を指定してください。

py.test -v test_sample.py

なお、通常は複数のテストプログラムをまとめて実行したくなるかと思います。pytestは、testから始まる名前のファイルをテスト対象として判断してくれるため、以下のようにファイル名を省略すれば、該当フォルダ内を探索してtestから始まる名前のファイルを順番にテストしてくれます。

py.test -v

また、テストプログラム内でprint等の標準出力をしている場合、上記のオプションだけでは個々のテストプログラムのprintは表示されません。print内容を表示したい場合は、以下のように「-s」オプションをつけて実行してください。

py.test -v -s

他にもpy.testのオプションとして指定できるものがありますので、以下コマンドでヘルプを確認してみてもらえるとよいかなと思います。

py.test --help

pytestによるテストの実装例

ここからは、より具体的にpytestを使用したテスト実装例を見ていきましょう。まずは、以降で説明する関連プログラムをまとめて示します。テスト対象のsample.py、テストプログラムのtest_sample.py、テスト用設定のconftest.pyがあります。詳細内容は後ほど順番に説明していきます。

sample.py:テスト対象プログラム

class Sample:
    """Sampleクラス"""

    def add_and_double(self, x, y):
        """xとyを足して2倍した値を返却する

        Args:
            x: 入力値1
            y: 入力値2
        """
        # intでない場合は、ValueErrorとする
        if not isinstance(x, int) or not isinstance(y, int):
            raise ValueError

        # 計算処理
        result = x + y
        result *= 2

        return result

test_sample.py:テストプログラム

import pytest

import sample


# スキップ用フラグ
SKIP_FLAG = True


class TestSample:
    """Sampleクラスのテスト用クラス"""

    @classmethod
    def setup_class(cls):
        print("\nStart TestSample")
        cls.temp = sample.Sample()

    @classmethod
    def teardown_class(cls):
        print("\nEnd of TestSample")
        del cls.temp

    def setup_method(self, method):
        print(f"\nStart method name: {method.__name__}")

    def teardown_method(self, method):
        print(f"\nEnd method name: {method.__name__}")

    def test_add_and_double(self):
        """テストケース1: 正常計算"""
        assert self.temp.add_and_double(1, 1) == 4
        assert self.temp.add_and_double(2, 2) == 8

    def test_add_and_double_raise(self):
        """テストケース2: 例外処理"""
        with pytest.raises(ValueError):
            self.temp.add_and_double("1", 1)
        with pytest.raises(ValueError):
            self.temp.add_and_double(1, "1")
        with pytest.raises(ValueError):
            self.temp.add_and_double("1", "1")

    @pytest.mark.skip(reason="skip理由:xxxxxxxxxx")
    def test_add_and_double_skip(self):
        """スキップ確認用"""
        assert self.temp.add_and_double(1, 1) == 4

    @pytest.mark.skipif(SKIP_FLAG is True, reason="条件付きskip理由:xxxxxxxxx")
    def test_add_and_double_skipif(self):
        """条件付きスキップ確認用"""
        assert self.temp.add_and_double(1, 1) == 4

    def test_add_and_double_option(self, request):
        """フィクスチャを使ってテストする場合
        ※requestはpytestで定義されているフィクスチャなのでこのように使うものと覚える

        Args:
            request: フィクスチャ
        """
        os_name = request.config.getoption("--os-name")
        if os_name == "windows":
            print("dir")
        elif os_name == "linux" or os_name == "mac":
            print("ls")
        assert self.temp.add_and_double(1, 1) == 4

    def test_add_and_double_original(self, target_numbers):
        """独自フィクスチャを使ってテストする場合

        Args:
            target_numbers: テスト対象数値リストを返す独自フィクスチャ
        """
        print(target_numbers)
        assert self.temp.add_and_double(1, 1) == 4

    def test_add_and_double_original_csv(self, open_csv_file):
        """独自フィクスチャを使ってテストする場合
        csvファイルなどのオープン/クローズをフィクスチャ側で実行

        Args:
            open_csv_file: csvファイルを開く独自フィクスチャ
        """
        print(open_csv_file)
        open_csv_file.write("test1,test2,test3")
        assert self.temp.add_and_double(1, 1) == 4

conftest.py:テスト用の設定

import pytest


def pytest_addoption(parser):
    """テストオプション用の関数"""
    parser.addoption("--os-name", default="windows", help="os name")


@pytest.fixture()
def target_numbers():
    """テスト対象数値リストを返却する

    Returns:
        テスト対象数値リスト
    """
    return [1, 5, 10]


@pytest.fixture()
def open_csv_file():
    """csvファイルをオープンして渡す
    yieldを使用することで終了時にファイルクローズする

    Yields:
        csvファイルオブジェクト
    """
    print("before open")
    with open("test.csv", "w+", encoding="utf-8") as csv_file:
        yield csv_file
    print("\nafter")

以下は、上記全てのフォルダを同一フォルダに配置し、当該フォルダでコマンドを実行した結果です。オプションは「-v」のみの場合と「-v -s」の場合の結果を示します。

実行結果「py.test -v」を実行

> py.test -v
=================================== test session starts ====================================
platform win32 -- Python 3.10.4, pytest-7.2.1, pluggy-1.0.0 -- D:\PythonProjects\python-tech-sample-source\python-tech-sample-source\venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\PythonProjects\python-tech-sample-source\python-tech-sample-source\python-libraries\pytest        
collected 7 items

test_sample.py::TestSample::test_add_and_double PASSED                                [ 14%]
test_sample.py::TestSample::test_add_and_double_raise PASSED                          [ 28%]
test_sample.py::TestSample::test_add_and_double_skip SKIPPED (skip理由:xxxxxxxxxx)    [ 42%]
test_sample.py::TestSample::test_add_and_double_skipif SKIPPED (条件付きskip理由:...) [ 57%]
test_sample.py::TestSample::test_add_and_double_option PASSED                         [ 71%]
test_sample.py::TestSample::test_add_and_double_original PASSED                       [ 85%]
test_sample.py::TestSample::test_add_and_double_original_csv PASSED                   [100%]

=============================== 5 passed, 2 skipped in 0.02s ===============================

実行結果 「py.test -v -s」を実行

> py.test -v -s
============================================ test session starts ============================================
platform win32 -- Python 3.10.4, pytest-7.2.1, pluggy-1.0.0 -- D:\PythonProjects\python-tech-sample-source\python-tech-sample-source\venv\Scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\PythonProjects\python-tech-sample-source\python-tech-sample-source\python-libraries\pytest        
collected 7 items                                                                                             

test_sample.py::TestSample::test_add_and_double
Start TestSample

Start method name: test_add_and_double
PASSED
End method name: test_add_and_double

test_sample.py::TestSample::test_add_and_double_raise
Start method name: test_add_and_double_raise
PASSED
End method name: test_add_and_double_raise

test_sample.py::TestSample::test_add_and_double_skip SKIPPED (skip理由:xxxxxxxxxx)
test_sample.py::TestSample::test_add_and_double_skipif SKIPPED (条件付きskip理由:xxxxxxxxx)
test_sample.py::TestSample::test_add_and_double_option
Start method name: test_add_and_double_option
dir
PASSED
End method name: test_add_and_double_option

test_sample.py::TestSample::test_add_and_double_original
Start method name: test_add_and_double_original
[1, 5, 10]
PASSED
End method name: test_add_and_double_original

test_sample.py::TestSample::test_add_and_double_original_csv
Start method name: test_add_and_double_original_csv
before open
<_io.TextIOWrapper name='test.csv' mode='w+' encoding='utf-8'>
PASSED
after

End method name: test_add_and_double_original_csv

End of TestSample


======================================= 5 passed, 2 skipped in 0.02s ========================================

以降では上記プログラムを使ってポイントとなるテスト記載方法を説明していきます。

テストの基本要素

テストの基本構成についてまず説明します。以下はtest_sample.py内のTestSampleクラスの一部抜粋です。

class TestSample:
    """Sampleクラスのテスト用クラス"""

    def test_add_and_double(self):
        """テストケース1: 正常計算"""
        assert self.temp.add_and_double(1, 1) == 4
        assert self.temp.add_and_double(2, 2) == 8

TestSampleというクラスを作成し、その中にテスト用メソッドをどんどん記載していきます。test_add_and_double(self)は、正常計算結果を確認することを意図して作成したテストメソッドです。

テスト結果の判定を行う場合には上記のようにassertを使って判定条件を記載します。

Python標準のテストフレームワークのunittestでは、TestCaseクラスを継承してself.assertEqualのようなメソッドで判定するため、判定メソッドを覚えたり調べたりといった点が面倒な点として挙げられます。一方で、pytestでは判定はPythonの比較演算を使用できるので直感的にテストを実装できるのがメリットです。

上記で記載しているケースは問題なく処理がされているため、実行結果を見ると以下のようにPASSEDとなります。

test_sample.py::TestSample::test_add_and_double PASSED

では、失敗する状況を考えてみます。例えば「assert self.temp.add_and_double(2, 2) == 4」のようなテストをした場合には以下のように失敗となります。なお、これは(2+2)×2=8ですので、テストケースが間違っている例です。

================================================= FAILURES ================================================== 
______________________________________ TestSample.test_add_and_double _______________________________________ 

self = <test_sample.TestSample object at 0x000002BEA713E0B0>

    def test_add_and_double(self):
        """テストケース1: 正常計算"""
        assert self.temp.add_and_double(1, 1) == 4
>       assert self.temp.add_and_double(2, 2) == 4
E       assert 8 == 4
E        +  where 8 = <bound method Sample.add_and_double of <sample.Sample object at 0x000002BEA713EDD0>>(2, 
2)
E        +    where <bound method Sample.add_and_double of <sample.Sample object at 0x000002BEA713EDD0>> = <sample.Sample object at 0x000002BEA713EDD0>.add_and_double
E        +      where <sample.Sample object at 0x000002BEA713EDD0> = <test_sample.TestSample object at 0x000002BEA713E0B0>.temp

test_sample.py:38: AssertionError

このように想定したものと出力結果が異なる場合にはFAILUREとして失敗が表示されます。

なお、テストの失敗としては「テスト対象プログラムの処理結果が誤っている場合」や上記のように「テストケース自体が誤っているような場合」等、色々ありますのでエラーを見つつ、プログラムを修正していくようにしてください。

例外処理

次に、例外処理をテストする場合のテストメソッドの記載方法です。

    def test_add_and_double_raise(self):
        """テストケース2: 例外処理"""
        with pytest.raises(ValueError):
            self.temp.add_and_double("1", 1)
        with pytest.raises(ValueError):
            self.temp.add_and_double(1, "1")
        with pytest.raises(ValueError):
            self.temp.add_and_double("1", "1")

今回のテスト対象プログラムsample.pyのadd_and_doubleメソッドでは、intではない引数が渡された場合にValueErrorを返却することになっています。

このような場合には、上記のようにpytest.raisesを使用したwith句を用いて確認します。もし「self.temp.add_and_double(1, 1)」のように例外をあげないような処理をwith句内に記載するとテストは失敗となります。

setupとteardown

次に、各テストの実行前後にテストのセットアップや完了時処理をするためのsetupteardownについての記載方法です。

    @classmethod
    def setup_class(cls):
        print("\nStart TestSample")
        cls.temp = sample.Sample()

    @classmethod
    def teardown_class(cls):
        print("\nEnd of TestSample")
        del cls.temp

    def setup_method(self, method):
        print(f"\nStart method name: {method.__name__}")

    def teardown_method(self, method):
        print(f"\nEnd method name: {method.__name__}")

setupは、テストの前に準備をするために用意されているものでsetup_classsetup_methodがあります。その名の通りクラス生成時のセットアップがsetup_classで、メソッドの実行時のセットアップがsetup_methodです。

一方で、teardownは、テストの後に処理を実行するために用意されているもので、teardown_classteardown_methodがあります。それぞれクラスとメソッドに対応します。

上記例では、テスト開始と終了、各メソッドの開始と終了が分かるようにprintしています。printは、py.testの実行時に-sオプションをつけた場合に表示できますが、実行結果を見ると以下のように表示されます。

> py.test -v -s

..(省略)...

test_sample.py::TestSample::test_add_and_double
Start TestSample

Start method name: test_add_and_double
PASSED
End method name: test_add_and_double

test_sample.py::TestSample::test_add_and_double_raise
Start method name: test_add_and_double_raise
PASSED
End method name: test_add_and_double_raise

...(省略)...

End of TestSample

setup_classとteardown_classのprintは最初と最後のみ表示されていることが分かります。一方でsetup_methodとteardown_methodのprintは各メソッドの実行前後で表示されています。

また、setup_classではあらかじめテスト対象のクラスSampleをオブジェクト化しており、teadown_classでは(特に必須なわけではないですが)オブジェクトをdelしています。このようにすることで、各テストメソッドでは毎回クラスをオブジェクト化する必要がなくなりますし、テスト終了時の処理を必要に応じて記載できます。

テスト前後のタイミングで何か処理を記載したい場合には、setupやteardownをうまく活用しましょう。

テストのスキップ

テストを多く記載していると一部関数やメソッドをスキップして処理を実行したくなる場合があります。テストをスキップする場合の記載方法について見ていきましょう。

通常のスキップ

一部のテストメソッドをスキップしたい場合には、以下のように@pytest.mark.skipデコレータを使用します。reasonにskip理由などを記載しておき実行時に表示することが可能です。

    @pytest.mark.skip(reason="skip理由:xxxxxxxxxx")
    def test_add_and_double_skip(self):
        """スキップ確認用"""
        assert self.temp.add_and_double(1, 1) == 4

スキップされると以下のような実行結果となります。

test_sample.py::TestSample::test_add_and_double_skip SKIPPED (skip理由:xxxxxxxxxx) 
条件付きスキップ

条件に一致する場合にテストをスキップしたいときもあるかと思いますが、その場合は以下のように@pytest.mark.skipifデコレータを使用して条件を引数に渡します。skip理由はreason引数に渡すことで表示することが可能です。

# スキップ用フラグ
SKIP_FLAG = True

...(省略)

    @pytest.mark.skipif(SKIP_FLAG is True, reason="条件付きskip理由:xxxxxxxxx")
    def test_add_and_double_skipif(self):
        """条件付きスキップ確認用"""
        assert self.temp.add_and_double(1, 1) == 4

スキップされると以下のような実行結果となります。

test_sample.py::TestSample::test_add_and_double_skipif SKIPPED (条件付きskip理由:xxxxxxxxx)

フィクスチャ(fixture)を使ったテスト

テストのフィクスチャ(fixture)とは、テストに必要な前提の状態や条件の集合の事を言います。テストコンテキストと呼ばれたりもします。

pytestでは、pytestで提供されているフィクスチャを使用したり、自分で独自フィクスチャを作成して使用したりすることもできるようになっています。pytestの「Fixtures reference」にフィクスチャに関するリファレンスページがあります。ページを見てもらうと分かりますが様々なビルトインのフィクスチャがあります。

pytestではconftest.pyという設定用のファイルを用意して使うことでフィクスチャをうまく扱っていくことができます。

以降では、pytestで提供されているフィクスチャの使用例の一例として「requestフィクスチャを使ってコマンドライン引数を扱う方法」と「独自のフィクスチャ作成方法」について紹介します。

requestフィクスチャを使ってコマンドライン引数を扱う方法

pytestで提供されているrequestフィクスチャを使って、py.testのコマンドライン引数を扱う方法を紹介します。まず、コマンドライン引数を使えるようにするには、以下のようにconftest.pyに記載します。

テスト用設定プログラム:conftest.py(対象箇所のみ抜粋)

"""pytestに関する各種設定を定義する"""
import pytest


def pytest_addoption(parser):
    """テストオプション用の関数"""
    parser.addoption("--os-name", default="windows", help="os name")

上記のようにpytest_addoption(parser)というメソッドを実装し、parser.addoptionにオプションを定義することで、py.testのコマンドライン引数として使用できるようになります。

上記の例では「–os-name」という引数を定義しています。また、引数省略時の設定としてdefault=”windows”と設定しています。helpでは「py.test –help」としてヘルプを表示した時の説明文を指定できます。実際にヘルプを表示してみると以下のようにCustom optionsとして表示されます。

> py.test --help
usage: py.test [options] [file_or_dir] [file_or_dir] [...]

...(省略)...

Custom options:
  --os-name=OS_NAME     os name

...(省略)...

では、実際にこのコマンドライン引数の入力をテスト側で使用するための記載方法を見ていきましょう。

test_sample.py:テストプログラム(対象箇所のみ抜粋)

    def test_add_and_double_option(self, request):
        """フィクスチャを使ってテストする場合
        ※requestはpytestで定義されているフィクスチャなのでこのように使うものと覚える

        Args:
            request: フィクスチャ
        """
        os_name = request.config.getoption("--os-name")
        if os_name == "windows":
            print("dir")
        elif os_name == "linux" or os_name == "mac":
            print("ls")
        assert self.temp.add_and_double(1, 1) == 4

テスト側でフィクスチャを使用する場合は、上記のようにメソッドの引数として受け取ります。requestはpytestで定義されているものなので、このように指定するものだと覚えてしまっていいかと思います。

先ほど追加したコマンドライン引数は「request.config.getoption(“–os-name”)」というようにすることで取得することができます。このようにして取得したコマンドライン引数の指定値をベースにテストの挙動を変えることができます。

例えば、–os-name=linux(macでも構いません)と指定して処理を実行するとテスト結果は以下のようになります。

> py.test -v -s --os-name=linux
...(省略)...

test_sample.py::TestSample::test_add_and_double_option
Start method name: test_add_and_double_option
ls
PASSED
End method name: test_add_and_double_option

...(省略)...

linuxもしくはmacと指定した場合は「ls」と表示するようにテストでは実装しているので、そのように表示されていることが分かります。–os-nameを省略した場合は、defaultは”windows”と定義したので「dir」と表示されます。

このようにconftest.pyとrequestフィクスチャをつかうことでコマンドライン引数を使ってテストを制御することが可能になります。

なお、pytestのフィクスチャは他にも色々あるためFixtures referenceを参考に色々調べてみてもらえるとよいかと思います。

独自フィクスチャの作成

pytestでは、上記で紹介したrequestのようにpytestが提供しているフィクスチャだけではなく、独自フィクスチャを作成して使用することが可能となっています。

独自フィクスチャを作成する場合には、conftest.py内に@pytest.fixture()のデコレータをつけた関数を定義することで実現できます。以降でいくつか例を使ってみてみましょう。

【例:数値リストの返却】

一つ目の例として、テスト対象の数値リストを返すようなフィクスチャを作成する例を以下で見てみましょう。まずは、conftest.pyにフィクスチャの関数を定義します。

テスト用設定プログラム:conftest.py(対象箇所のみ抜粋)

"""pytestに関する各種設定を定義する"""
import pytest


@pytest.fixture()
def target_numbers():
    """テスト対象数値リストを返却する

    Returns:
        テスト対象数値リスト
    """
    return [1, 5, 10]

@pytest.fixture()デコレータをつけてtarget_numbersという数値リストを返すだけの関数を定義しています。

では、実際にこのフィクスチャをテスト側で使用するための記載方法を見ていきましょう。

テストプログラム:test_sample.py(対象箇所のみ抜粋)

    def test_add_and_double_original(self, target_numbers):
        """独自フィクスチャを使ってテストする場合

        Args:
            target_numbers: テスト対象数値リストを返す独自フィクスチャ
        """
        print(target_numbers)
        assert self.temp.add_and_double(1, 1) == 4

テストを実行する側では上記で紹介したrequestの例と同様にテスト関数で作成したフィクスチャ関数名を「def test_add_and_double_original(self, target_numbers):」のように指定します。

実際の実行結果は以下のようになります。

test_sample.py::TestSample::test_add_and_double_original
Start method name: test_add_and_double_original
[1, 5, 10]
PASSED
End method name: test_add_and_double_original

target_numbersをprintした結果を見てみると[1, 5, 10]という関数の戻り値が表示されていることが分かります。このようにテストで使用する数値リスト等を返却するといったことが独自フィクスチャだとできるようになります。

今回は数値リストを返すような簡単な例でフィクスチャを定義しましたが、どのような定義も可能です。このように独自フィクスチャでテスト条件となる値等を定義しておくと複数のテストメソッドで使用できるようになり、テスト条件とテスト実装をうまく分離することができるようになります。

【例:ファイルオープン/クローズをフィクスチャ側で実行】

独自フィクスチャの作り方として、もう1つ面白い使い方をご紹介します。テストで使用するcsvファイルをオープンして渡すような以下のような独自フィクスチャを作ることを考えてみます。

テスト用設定プログラム:conftest.py(対象箇所のみ抜粋)

@pytest.fixture()
def open_csv_file():
    """csvファイルをオープンして渡す
    yieldで返すことで終了時にファイルクローズする

    Yields:
        csvファイルオブジェクト
    """
    print("before open")
    with open("test.csv", "w+", encoding="utf-8") as csv_file:
        yield csv_file
    print("\nafter")

上記はtest.csvファイルをオープンしてオブジェクトを返却する関数ですが、ポイントは、with句を使ってyieldでオブジェクトを返している点です。このようにすると、テスト側でファイルの使用が終わった際にwith句を抜けるのでファイルが自動的にクローズされます。

では、実際にこのフィクスチャをテスト側で使用するための記載方法を見ていきましょう。

テストプログラム:test_sample.py(対象箇所のみ抜粋)

    def test_add_and_double_original_csv(self, open_csv_file):
        """独自フィクスチャを使ってテストする場合
        csvファイルなどのオープン/クローズをフィクスチャ側で実行

        Args:
            open_csv_file: csvファイルを開く独自フィクスチャ
        """
        print(open_csv_file)
        open_csv_file.write("test1,test2,test3")
        assert self.temp.add_and_double(1, 1) == 4

使い方は、これまで紹介してきたフィクスチャと同じです。わたってきたオブジェクトを使ってwriteで書き込みを行っています。

実際の実行結果は以下のようになります。

Start method name: test_add_and_double_original_csv
before open
<_io.TextIOWrapper name='test.csv' mode='w+' encoding='utf-8'>
PASSED
after

今回with句を抜けたことが分かりやすいように、”after”という文字列を表示しています。このようにフィクスチャ側でファイルのオープン/クローズを実行することが可能です。

まとめ

Pythonプログラムのテストを実行するためのフレームワークであるpytestモジュールについて解説しました。pytestは、Python標準テストフレームワークのunittestモジュールの機能制限などに不満を持つ開発者によって開発されたテストフレームワークの一つです。

本記事内では、pytestの実行方法やテスト実装例について紹介してきました。setupやteadownを使ったテスト準備やテストのスキップ、フィクスチャをつかったテストについて紹介しています。

pytestは、Pythonのテストフレームワークとして非常に有用なパッケージです。皆さんも色々と使い方を調べてもらうと面白いかと思います。

また、テスト駆動開発(TDD:Test-Driven Developmentという自動テストを中心にした高品質のソフトウェア開発をするための方法論はPythonコミュニティでは幅広く利用されています。pytestはテスト駆動開発においても有用なテストフレームワークです。テスト駆動開発については「エキスパートPythonプログラミング」という書籍にも記載がありますので、興味があれば参考にしてもらえるとよいかと思います。