Pytest05-Fixture

19344 ワード

5.Fixture


テスト中はfixtureが役に立ちます.fixtureは、テスト関数の実行前後にpytestが実行するハウジング関数です.fixtureのコードはカスタマイズでき、多変のテストニーズを満たすことができ、テストに入力されたデータセットを定義し、テスト前のシステムの初期状態を構成し、一括テストにデータソースを提供するなどを含む.次の簡単な例を見て、簡単なfixtureを返します.
import pytest

@pytest.fixture()
def getData():
    return 28

def test_getFixture(getData):
    assert getData==28
  • @pytest.fixture()アクセラレータは、関数がfixtureであることを宣言するために使用されます.テスト関数のパラメータリストにfixture名が含まれている場合、pytestは検出し、テスト関数が実行される前にfixtureを実行します.fixtureは、タスクを完了したり、テスト関数にデータを返したりすることができます.
  • test_getFixture()のパラメータリストにはgetDataというfixtureが含まれており、pytestはその名前でfixtureを検索します.pytestは、テストが存在するモジュールを優先的に検索し、conftestを検索します.py

  • 後述fixtureは@pytest.fixture()アクセラレータ定義の関数であり、fixtureはpytestがテスト前後に予備・クリーンアップ作業を行うコードをコアロジックから分離するメカニズムである.

    5.1 conftestを通過する.py共有fixture


      fixtureの特徴は以下の通りです.
  • 1.fixtureは、個別のテストファイルに置くことができます.複数のテストファイルでfixtureを共有する場合は、共通ディレクトリにconftestを新規作成できます.pyファイル、fixtureを配置します.
  • 2.fixtureの有効範囲をテストファイルに限定する場合は、fixtureをテストファイルに
  • と書くことができます.
  • 3.だがpyはPythonモジュールですが、テストファイルにインポートできません.したがってimport conftestの
  • は認められない.

    5.2 fixtureを使用した構成および破棄ロジックの実行


      テスト前の準備とテスト終了後に環境をクリーンアップし、データベースへの接続で使用することが多い.テスト前にデータベースに接続する必要があり、テストが完了した後、データベースを閉じる必要があるなど、fixtureを使用して環境の構成とクリーンアップを行うことができます.以下に示します.
    1.DBOperate.py
    import sqlite3
    import os
    
    class DBOperate:
    
        def __init__(self,dbPath=os.path.join(os.getcwd(),"db")):
            self.dbPath=dbPath
            self.connect=sqlite3.connect(self.dbPath)
    
        def Query(self,sql:str)->list:
            """ """
            queryResult = self.connect.cursor().execute(sql).fetchall()
            return queryResult if queryResult else []
    
        def QueryAsDict(self,sql:str)->dict:
            """ """
            self.connect.row_factory=self.dictFactory
            cur=self.connect.cursor()
            queryResult=cur.execute(sql).fetchall()
            return queryResult if queryResult else {}
    
        def Insert(self,sql:str)->bool:
            insertRows=self.connect.cursor().execute(sql)
            self.connect.commit()
            return True if insertRows.rowcount else False
    
        def Update(self,sql:str)->bool:
            updateRows=self.connect.cursor().execute(sql)
            self.connect.commit()
            return  True if updateRows.rowcount else False
    
    
        def Delete(self,sql:str)->bool:
            delRows=self.connect.cursor().execute(sql)
            self.connect.commit()
            return True if delRows.rowcount else False
    
        def CloseDB(self):
            self.connect.cursor().close()
            self.connect.close()
    
        def dictFactory(self,cursor,row):
            """ sql """
            d={}
            for index,col in enumerate(cursor.description):
                d[col[0]]=row[index]
            return d
    

    2.conftest.py
    import pytest
    from DBOperate import DBOperate
    
    @pytest.fixture()
    def dbOperate():
        # setup:connect db
        db=DBOperate()
        #  
        sql="""SELECT * FROM user_info"""
        res=db.QueryAsDict(sql)
        # tearDown:close db
        db.CloseDB()
        return res
    

    3.test_02.py
    import pytest
    from DBOperate import DBOperate
    
    def test_dbOperate(dbOperate):
        db=DBOperate()
        sql = """SELECT * FROM user_info"""
        expect=db.QueryAsDict(sql)
        res=dbOperate
        assert expect==res
    

      fixtureでは、クエリー文を実行する前にdb=DBPerate()はデータベース接続を確立することに相当し、構成プロセス(setup)と見なすことができるが、db.CloseDB()はクリーンアッププロセス(teardown)プロセスに相当し、テストプロセスで何が起こってもクリーンアッププロセスが実行されます.

    5.3--setup-showを使用してfixture実行プロセスを遡及する


     前のテストを直接実行すると、fixtureの実行プロセスは次のように表示されません.
    >>> pytest -v .\test_02.py
    =================== test session starts =================================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe
    cachedir: .pytest_cache
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 1 item
    
    test_02.py::test_dbOperate PASSED                                   [100%]
    
    ===================== 1 passed in 0.07s ==================================
    

      詳細な実行手順および実行の前後順序を表示するには、次のようにパラメータ--setup-showを使用します.
    >>> pytest  --setup-show -v .\test_02.py
    ====================== test session starts ==================================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe
    cachedir: .pytest_cache
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 1 item
    
    test_02.py::test_dbOperate
            SETUP    F dbOperate
            test_02.py::test_dbOperate (fixtures used: dbOperate)PASSED
            TEARDOWN F dbOperate
    
    ============================ 1 passed in 0.03s =================================
    

    上の実行の出力結果から、本物のテスト関数が挟まれており、pytestは各fixtureの実行をsetupとteardownの2つの部分に分けていることがわかります.
    fixture名の前のFはその作用範囲を表し、F:関数レベルを表し、S:セッションレベルを表す

    5.4 fixtureを使用してテストデータを転送する


     fixtureはテストデータを格納するのに非常に適しており、以下に示すように、任意のデータを返すことができます.
    import pytest
    
    @pytest.fixture()
    def sampleList():
        return [1,23,"a",{"a":1}]
    
    def test_sampleList(sampleList):
        assert sampleList[1]==32
    

    実行結果は次のとおりです.
    >>> pytest -v .\test_fixture.py
    =========================== test session starts ==============================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe
    cachedir: .pytest_cache
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 1 item
    
    test_fixture.py::test_sampleList FAILED                                         [100%]
    
    ================================= FAILURES ===========================================
    _________________________test_sampleList ____________________________________________
    
    sampleList = [1, 23, 'a', {'a': 1}]
    
        def test_sampleList(sampleList):
    >       assert sampleList[1]==32
    E       assert 23 == 32
    E         +23
    E         -32
    
    test_fixture.py:8: AssertionError
    ===========================short test summary info =================================
    FAILED test_fixture.py::test_sampleList - assert 23 == 32
    =========================== 1 failed in 0.20s ======================================
    

    詳細なエラー情報を示すほか、pytestはassert異常を引き起こす関数パラメータ値を与える.fixtureはテスト関数のパラメータとして、テストレポートにも組み込まれます.上の例では、テスト関数で異常が発生することを示していますが、fixtureで異常が発生した場合はどうなりますか?
    import pytest
    
    @pytest.fixture()
    def sampleList():
        x=23
        assert x==32
        return x
    
    def test_sampleList(sampleList):
        assert sampleList==32
    

    実行結果は次のとおりです.
    >>> pytest -v .\test_fixture.py
    ======================= test session starts =============================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe
    cachedir: .pytest_cache
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 1 item
    
    test_fixture.py::test_sampleList ERROR                                     [100%]
    
    ========================== ERRORS ==========================================
    _________________ ERROR at setup of test_sampleList ________________________
    
        @pytest.fixture()
        def sampleList():
            x=23
    >       assert x==32
    E       assert 23 == 32
    E         +23
    E         -32
    
    test_fixture.py:6: AssertionError
    ==================== short test summary info ================================
    ERROR test_fixture.py::test_sampleList - assert 23 == 32
    =========================== 1 error in 0.27s =================================
    

      実行した出力結果でfixture関数でassert異常が発生した位置に正しく位置決めし、次にtest_sampleListはFAILとしてマークされていないがERRORとしてマークされている.この区分は、FAILとマークされている場合、テスト依存のfixtureではなくコア関数で失敗が発生していることをユーザが知っていることが明らかである.

    5.5複数のfixtureを使用する


      サンプルコードは次のとおりです.
    1.DBOperate
    import sqlite3
    import os
    
    class DBOperate:
    
        def __init__(self,dbPath=os.path.join(os.getcwd(),"db")):
            self.dbPath=dbPath
            self.connect=sqlite3.connect(self.dbPath)
    
        def Query(self,sql:str)->list:
            """ """
            queryResult = self.connect.cursor().execute(sql).fetchall()
            return queryResult if queryResult else []
    
        def QueryAsDict(self,sql:str)->dict:
            """ """
            self.connect.row_factory=self.dictFactory
            cur=self.connect.cursor()
            queryResult=cur.execute(sql).fetchall()
            return queryResult if queryResult else {}
    
        def Insert(self,sql:str)->bool:
            insertRows=self.connect.cursor().execute(sql)
            self.connect.commit()
            return True if insertRows.rowcount else False
    
        def Update(self,sql:str)->bool:
            updateRows=self.connect.cursor().execute(sql)
            self.connect.commit()
            return  True if updateRows.rowcount else False
    
    
        def Delete(self,sql:str)->bool:
            delRows=self.connect.cursor().execute(sql)
            self.connect.commit()
            return True if delRows.rowcount else False
    
        def CloseDB(self):
            self.connect.cursor().close()
            self.connect.close()
    
        def dictFactory(self,cursor,row):
            """ sql """
            d={}
            for index,col in enumerate(cursor.description):
                d[col[0]]=row[index]
            return d
    

    2.conftest.py
    import pytest
    from DBOperate import DBOperate
    
    @pytest.fixture()
    def dbOperate():
        # setup:connect db
        db=DBOperate()
        yield
        # tearDown:close db
        db.CloseDB()
    
    @pytest.fixture()
    def mulQuerySqlA():
        return (
            "SELECT * FROM user_info",
            "SELECT * FROM case_info",
            "SELECT * FROM config_paras"
        )
    
    @pytest.fixture()
    def mulQuerySqlB():
        return (
            "SELECT * FROM user_info WHERE account in('admin')",
            "SELECT * FROM case_info WHERE ID in('TEST-1')",
            "SELECT * FROM config_paras WHERE accountMinChar==2",
            "SELECT * FROM report_info WHERE ID in('TEST-1')"
        )
    
    @pytest.fixture()
    def mulFixtureA(dbOperate,mulQuerySqlA):
        db = DBOperate()
        tmpList=[]
        for item in mulQuerySqlA:
            tmpList.append(db.QueryAsDict(item))
        return tmpList
    
    @pytest.fixture()
    def mulFixtureB(dbOperate,mulQuerySqlB):
        db = DBOperate()
        tmpList = []
        for item in mulQuerySqlB:
            tmpList.append(db.QueryAsDict(item))
        return tmpList
    

    3.test_03.py
    import pytest
    
    def test_count(mulQuerySqlA):
        assert len(mulQuerySqlA)==3
    

    実行結果は次のとおりです.
    >>> pytest -v --setup-show .\test_03.py
    ========================= test session starts ================================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe
    cachedir: .pytest_cache
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 1 item
    
    test_03.py::test_count
            SETUP    F mulQuerySqlA
            test_03.py::test_count (fixtures used: mulQuerySqlA)PASSED
            TEARDOWN F mulQuerySqlA
    
    ========================== 1 passed in 0.05s ==================================
    

      fixtureを使用する利点は、ユーザーが作成したテスト関数は、テスト前の準備作業を考慮することなく、コアのテストロジックのみを考慮できることです.

    5.6 fixture作用範囲の指定


     fixtureにはscopeというオプションパラメータがあり、fixtureがいつ構成および破棄ロジックを実行するかを制御するのによく使用される役割範囲と呼ばれます[email protected]()には通常、function、class、module、sessionの4つのオプション値があり、デフォルトはfunctionです.各scopeの説明情報は以下の通りです.
  • 1.scope="function"

  •   関数レベルのfixtureは、各テスト関数で1回のみ実行され、構成コードはテスト関数の実行前に実行され、クリーンアップコードはテスト関数の実行後に実行されます.
  • 2.scope="class"

  • クラスレベルのfixtureは、クラスごとに1回のみ実行され、クラスにいくつのテストメソッドがあるかにかかわらず、このfixtureを共有することができる.
  • 3.scope="moudle"

  •  モジュールレベルのfixture各モジュールは1回のみ実行され、モジュールに複数のテスト関数、テスト方法、または他のfixtureが存在してもこのfixtureを共有できます.
  • 4.scope="session"

  • セッションレベルのfixtureは、セッションごとに1回のみ実行され、1回のセッションでは、すべてのテストメソッドとテスト関数がこのfixtureを共有できます.
      各作用範囲のscope例を以下に示す.
    import pytest
    
    @pytest.fixture(scope="function")
    def funcScope():
        pass
    
    @pytest.fixture(scope="module")
    def moduleScope():
        pass
    
    @pytest.fixture(scope="session")
    def sessionScope():
        pass
    
    @pytest.fixture(scope="class")
    def classScope():
        pass
    
    def test_A(sessionScope,moduleScope,funcScope):
        pass
    
    @pytest.mark.usefixtures("classScope")
    class TestSomething:
        def test_B(self):
            pass
        def test_C(self):
            pass
    

    実行結果は次のとおりです.
    >>> pytest --setup-show -v .\test_scope.py
    =================== test session starts ===================================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe
    cachedir: .pytest_cache
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 3 items
    
    test_scope.py::test_A
    SETUP    S sessionScope
        SETUP    M moduleScope
            SETUP    F funcScope
            test_scope.py::test_A (fixtures used: funcScope, moduleScope, sessionScope)PASSED
            TEARDOWN F funcScope
    test_scope.py::TestSomething::test_B
          SETUP    C classScope
            test_scope.py::TestSomething::test_B (fixtures used: classScope)PASSED
    test_scope.py::TestSomething::test_C
            test_scope.py::TestSomething::test_C (fixtures used: classScope)PASSED
          TEARDOWN C classScope
        TEARDOWN M moduleScope
    TEARDOWN S sessionScope
    
    =========================3 passed in 0.04s ===============================
    

      以上の各アルファベットは、異なるscopeレベル、C(class)、M(module)、F(function)、S(Session)を表す.
    fixtureでは、同じレベルまたは自分より高いレベルのfixtureしか使用できません.たとえば、関数レベルのfixtureでは、同じレベルのfixtureを使用するか、クラスレベル、モジュールレベル、セッションレベルのfixtureを使用するか、逆にできません.

    5.7 usefixtureでfixtureを指定する


      テスト関数リストにfixtureを指定するほか、@pytest.mark.usefixtures(「fixture 1」、「fixture 2」)は、テスト関数またはクラスをマークします.このマーキング方法はテストクラスに非常に適している.次のようになります.
    @pytest.fixture(scope="class")
    def classScope():
        pass
    
    @pytest.mark.usefixtures("classScope")
    class TestSomething:
        def test_B(self):
            pass
        def test_C(self):
            pass
    

    usefixturesとテストメソッドにfixtureパラメータを追加する方法では、両者に大きな違いはありません.唯一の違いは、後者がfixtureの戻り値を使用できることです.

    5.8 fixtureにautouseオプションを追加する


      以前に使用したfixtureは、テスト自体に基づいて名前を付けたり、例のテストクラスに対してusefixturesを使用したりすることができます.autouse=Trueオプションを指定することで、作用範囲内のテスト関数がfixtureを実行するようにすることもできます.この方法は、複数回実行する必要がありますが、システム状態や外部データに依存しないコードに適しています.サンプルコードは次のとおりです.
    import pytest
    import time
    
    @pytest.fixture(autouse=True,scope="session")
    def endSessionTimeScope():
        yield
        print(f"
    finished {time.strftime('%Y-%m-%d %H:%M:%S')}") @pytest.fixture(autouse=True) def timeDeltaScope(): startTime=time.time() yield endTime=time.time() print(f"
    test duration:{round(endTime-startTime,3)}") def test_A(): time.sleep(2) def test_B(): time.sleep(5)

    実行結果は次のとおりです.
    >>> pytest -v -s .\test_autouse.py
    ==================== test session starts =======================================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe
    cachedir: .pytest_cache
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 2 items
    
    test_autouse.py::test_A PASSED
    test duration:2.002
    
    test_autouse.py::test_B PASSED
    test duration:5.006
    
    finished 2020-05-26 12:35:57
    
    =============================2 passed in 7.13s ====================================
    

    5.9 fixtureの名前を変更する


     fixtureの名前は、通常、そのテストまたは他のfixture関数を使用するパラメータリストに表示され、一般的にfixture関数名と一致します.pytestでは@pytestも使用できます.fixture(name="fixtureName")はfixtureの名前を変更します.例は次のとおりです.
    import pytest
    
    @pytest.fixture(name="Surpass")
    def getData():
        return [1,2,3]
    
    def test_getData(Surpass):
        assert Surpass==[1,2,3]
    

      先の例ではfixture名を使用する場合は関数名を使用し、@pytestを使用する.fixture(name="Surpass")の場合、fixtureに別名を付けることになります.fixtureを呼び出すと、別名を使用できます.実行結果は次のとおりです.
    >>> pytest --setup-show .\test_renamefixture.py
    =======================test session starts ===============================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 1 item
    
    test_renamefixture.py
            SETUP    F Surpass
            test_renamefixture.py::test_getData (fixtures used: Surpass).
            TEARDOWN F Surpass
    
    =========================== 1 passed in 0.05s ===============================
    

     名前変更後のfixture定義を見つけるには、pytestのオプションであるfixturesを使用し、テストファイル名を指定します.pytestは、名前の変更を含むすべてのテストで使用されるfixtureを提供します.以下に示します.
    >>> pytest --fixtures .\test_renamefixture.py
    ========================test session starts =================================================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 1 item
    --------------------------- fixtures defined from conftest ----------------------------------
    mulQuerySqlA
        conftest.py:14: no docstring available
    
    mulQuerySqlB
        conftest.py:22: no docstring available
    
    mulFixtureA
        conftest.py:32: no docstring available
    
    mulFixtureB
        conftest.py:40: no docstring available
    
    dbOperate
        conftest.py:5: no docstring available
    
    
    ------------------------- fixtures defined from test_renamefixture -------------------------
    Surpass
        test_renamefixture.py:4: no docstring available
    

    5.10 fixtureパラメータ化


      は4.7でテストのパラメータ化を紹介したが、fixtureに対してパラメータ化処理を行うこともできる.次のようにfixtureパラメータ化の別の機能を示します.
    import pytest
    
    paras=((1,2),(3,5),(7,8),(10,-98))
    parasIds=[f"{x},{y}" for x,y in paras]
    
    def add(x:int,y:int)->int:
        return x+y
    @pytest.fixture(params=paras,ids=parasIds)
    def getParas(request):
        return request.param
    
    def test_add(getParas):
        res=add(getParas[0],getParas[1])
        expect=getParas[0]+getParas[1]
        assert res==expect
    

     fixtureパラメータリストのrequestもpytestに組み込まれたfixtureの1つであり、fixtureの呼び出し状態を表します.getParasロジックはrequestのみで非常に簡単です.paramは戻り値としてテスト用に使用され、parasには4つの要素があるため、4回呼び出される必要があり、実行結果は以下の通りです.
    >>> pytest -v .\test_fixtrueparamize.py
    ============================ test session starts ================================
    platform win32 -- Python 3.7.6, pytest-5.4.2, py-1.8.1, pluggy-0.13.1 -- d:\program files\python\python.exe
    cachedir: .pytest_cache
    rootdir: C:\Users\Surpass\Documents\PycharmProjects\PytestStudy\Lesson03
    collected 4 items
    
    test_fixtrueparamize.py::test_add[1,2] PASSED                               [ 25%]
    test_fixtrueparamize.py::test_add[3,5] PASSED                               [ 50%]
    test_fixtrueparamize.py::test_add[7,8] PASSED                               [ 75%]
    test_fixtrueparamize.py::test_add[10,-98] PASSED                            [100%]
    
    ================================ 4 passed in 0.10s =====================================