zoukankan      html  css  js  c++  java
  • Pytest05-Fixture

    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()的参数列表中包含一个fixture,名为getData,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.尽管conftest.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=DBOperate()相当于建立数据库连接,可视为配置过程(setup),而db.CloseDB()则相当于清理过程(teardown)过程,无论测试过程发生了什么,清理过程均会被执行。

    5.3 使用--setup-show回溯fixture执行过程

        如果直接运行前面的测试,则看不到其fixture的执行过程,如下所示:

    >>> pytest -v .	est_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 filespythonpython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    collected 1 item
    
    test_02.py::test_dbOperate PASSED                                   [100%]
    
    ===================== 1 passed in 0.07s ==================================
    

        如果希望看到其详细的执行过程及执行的先后顺序,可以使用参数--setup-show,如下所示:

    >>> pytest  --setup-show -v .	est_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 filespythonpython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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两部分。

    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 .	est_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 filespythonpython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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 .	est_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 filespythonpython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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 .	est_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 filespythonpython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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何时执行配置和销毁逻辑。@pytest.fixture()通常有4个可选值,分别为functionclassmodulesession,默认为function。各个scope的描述信息如下所示:

    • 1.scope="function"

        函数级别的fixture每个测试函数仅运行一次,配置代码在测试函数运行之前运行,清理代码则在测试函数运行之后运行。

    • 2.scope="class"

        类级别的fixture每个测试类仅运行一次,无论类中有多少个测试方法,都可以共享这个fixture.

    • 3.scope="moudle"

        模块级别的fixture每个模块仅运行一次,无论模块中有多少个测试函数、测试方法或其他fixture都可以共享这个fixture

    • 4.scope="session"

        会话级别的fixture每个会话仅运行一次,一次会话中,所有测试方法和测试函数都可以共享这个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 .	est_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 filespythonpython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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("fixture1","fixture2")标记测试函数或类。这种标记方法对测试类非常适用。如下所示:

    @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 .	est_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 filespythonpython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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 .	est_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:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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 .	est_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:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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之一,代表fixture的调用状态。getParas逻辑非常简单,仅以request.param做为返回值供测试用,paras里面有4个元素,因此需要被调用4次,运行结果如下所示:

    >>> pytest -v .	est_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 filespythonpython.exe
    cachedir: .pytest_cache
    rootdir: C:UsersSurpassDocumentsPycharmProjectsPytestStudyLesson03
    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 =====================================
    

    原文地址:https://www.cnblogs.com/surpassme/p/13258521.html

    本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:
    MyQRCode.jpg

  • 相关阅读:
    web项目经理手册【2】开发时间估算
    DML需手动提交事务,DCL和DDL自动提交事务
    web项目经理手册【5】项目经理的工作内容
    如何选择最合适的Web开发框架
    微软放出首款开源博客内容管理系统Oxite
    ASP.NET实用技巧
    泛型类型的子类及通配符的使用
    ASP.NET最近遇上的问题小结
    oracle中的集合操作符
    Web测试概述
  • 原文地址:https://www.cnblogs.com/surpassme/p/13258521.html
Copyright © 2011-2022 走看看