pytest笔记

安装pytest

  • 安装
pip install -U pytest
  • 查验
# 查看version
pytest --version

# 查看包的安装位置
pip list 
pip show pytest
  • 第一个测试用例
    • 新建py文件,文件名需要test_开头,如test_sample.py
    • 使用断言assert
    • 用例完成后,将执行路径cd当当前文件所在路径下,然后终端输入pytest执行该用例;
# 测试用例如下
def fun(x):
    return x +1 

def test_answer():
    assert fun(3) == 5
  • 总结:从上面的测试来看,在写测试case的时候只需要把测试方法或者测试类写出来就行,不用去实现方法的调用,终端敲入pytest test_sample.py的时候自然会实现方法的调用。

运行pytest生成的缓存文件

  • 运行pytest之后,会自动生成如下文件:
    • __pycache__:其中存放的是.pyc文件。
    • .pytest_cache文件。
  • 这些文件是缓存文件,工作原理如下:
    • python的基本运行机制:程序运行时不需要编译成二进制代码,而直接从源码运行程序,简单说来就是python解释器把源码变成字节码,再由解释器来执行这些字节码。第一次执行代码的时候,python解释器把已经编译过把字节码放在__pycache__,当再次调用的时候,如果被调用模块未发生改变,则跳过该步骤,直接去__pycache__文件夹中运行相关的.pyc文件,大大缩短项目运行前的前期准备时间。
  • 从python的显式main()函数的角度来理解解释器执行调用模块时的行为:
# py文件1,tt1.py
import tt2
print("xixixix")

# py文件2,tt2.py
def haha():
    print("hahaha")
if __name__ == "__main__":
    haha()
    print(__name__)

# 如果从tt2.py进入,则打印:
hahaha
__main__

#如果从tt1.py进入,则打印:
xixixix

# 原因:python文件执行的入口为隐式的main()函数,所以在tt2中加if判断,但是从tt1执行,结果会只执行tt1的内容;

# 如果tt2中去掉if条件,依旧从tt1进入,这时候打印出来的__name__是tt2,如下:
hahaha
tt2
xixixix

# 可以看出python在执行入口文件之前,会把执行入口文件import的内容先扫描一遍,这也是pytest用缓存来加快执行速度的原因。

pytest编写测试用例的规则

  • 测试文件以test_开头;
  • 测试类必须以Test开头;
  • 执行测试用例:cd当测试文件所在文件夹,如果要执行所有的testcase,直接不带任何参数用pytest,会查找当前目录及子目录下所有test_开头的py文件,执行里面满足测试用例规则的测试方法和测试类。
  • 根据文件名执行测试用例:cd当测试文件所在文件夹,用pytest test_example.py;
  • 执行特定测试用例:pytest -s -v -k test_testfunction
    • -s:禁止捕获标准输出(stdout)和标准错误(stderr),使终端更简洁,便于调试;
    • -v:详细输出;
    • -k:以匹配关键字的方式指定执行特定的测试用例;

setup和teardown

  • fixture:夹具,用法:在函数前加装饰器@pytest.fixture()标记为夹具;
import pytest

@pytest.fixture()
def login():
    print("login")
    return 8

class Test_demo:
    def test_case1(self):
        print("execute test case 1:")
        assert 1+1==2

    def test_case2(self,login):         # 夹具只在此处被手动调用
        print("execute test case 2:")
        print(login)
        assert 1+login==10

    def test_case3(self):
        print("execute test case 3:")
        assert 99+1==100
  • 练习过程中出现问题debug:若测试中出现夹具没有被正确调用,另一方面新建的虚拟环境python -m venv venv,调用虚拟环境PS D:\temp\venv> .\Scripts\Activate.ps1也报错,报错出现的原因及解决办法:
File C:\Temp\Test.ps1 cannot be loaded because the execution of scripts is disabled on this system. Please see "get- help about_signing" for more details.

At line:1 char:19
+ c:\Temp\Test.ps1 <<<<

step1:用管理员方式打开powershell
step2:Get-ExecutionPolicy -List获取有效执行策略;
step3:发现大部分是Undefined或者RemotedSigned;
step4:用Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope LocalMachine把LocalMachine改成Bypass,增加使用权限,然后enter,然后按Y即可解决以上问题;
微软官方关于执行策略更改的说明

  • 夹具的作用范围:

    • function:@pytest.fixture(scope="function")每个函数或者方法都会调用一遍夹具;
    • class:@pytest.fixture(scope="class")每个类调用一次夹具;
    • module:@pytest.fixture(scope="module")每个.py文件调用一次夹具;
    • session:@pytest.fixture(scope="session")覆盖整个执行过程;
  • yield:实现teardown,yield之前的的代码在case之前执行,之后的代码在case运行后执行。

  • 显式调用夹具@pytest.mark.usefixtures("fixture_name"):

import pytest

@pytest.fixture(scope="class")              # 设置夹具作用范围
def logstatus():
    print("login")
    yield                                   # 设置teardown
    print("logout")

@pytest.mark.usefixtures("logstatus")       # 显式调用夹具,也可以把它放到特定测试方法前面,作用范围则变成该方法执行期间。
class Test_demo:
    def test_case1(self):
        print("execute test case 1:")
        assert 1+1==3

    def test_case2(self):
        print("execute test case 2:")
        assert 1+8==7

    def test_case3(self):                   # self表示实例对象本身,不加会出现TypeError: Test_demo.test_case3() takes 0 positional arguments but 1 was given错误
        print("execute test case 3:")
        assert 99+1==10
---------------------------------------------------------------------------------------------- Captured stdout setup ----------------------------------------------------------------------------------------------
login
---------------------------------------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------------------------------------
execute test case 1:
-------------------------------------------------------------------------------------------- Captured stdout teardown ---------------------------------------------------------------------------------------------
logout
  • fixture自动应用,为了省略显式调用,可以选择把夹具标记为autouse
    • autouse设置为true时,自动调用fixture,其默认作用域为function,不指定scope则每个方法都会调用fixture。
import pytest

@pytest.fixture(autouse=True,scope="function")
def logstatus():
    print("login")
    yield
    print("logout")

# @pytest.mark.usefixtures("logstatus")
class Test_demo:
    def test_case1(self):
        print("execute test case 1:")
        assert 1+1==3

    def test_case2(self):
        print("execute test case 2:")
        assert 1+8==7

    def test_case3(self):
        print("execute test case 3:")
        assert 99+1==10
  • fixture通过params可以传递参数,如:@pytest.fixture(scope="module",params=[]).

conftest共享fixture函数

  • 在测试过程中,多个测试文件都可能调用fixture,这时候如果将fixture移到conftest.py中,则不需要再在测试函数中导入(import),pytest自动识别conftest中的内容,查找顺序从测试类开始,然后是测试模块,然后是conftest.py文件,最后是内置插件和三方插件。

pytest实现不同参数的重复测试:

  • @pytest.mark.parametrize("param_in",params);
    • 假如params=[1,2,3],在下面的代码执行:
    • pytest会运行三次,分别带入params=1,=2,=3
@pytest.mark.parametrize("param_in",params)
def test_example(param_in)
    print(print)

pytest hook的实现和使用

  • hook装饰器一般用于报告的生成,把case执行前后的状态累加的hook函数中:
    • 当hookwrapper=True允许在hook的前后插入逻辑,通过yield在hook执行的前后分别添加自定义代码。
# conftest.py中的内容

import pytest
@pytest.hookimpl(hookwrapper=True)          # hookwrapper=True表示该hook是一个包装器
def pytest_runtest_makereport(item,call):
    # 在测试用例之前执行
    print(f"start run test case:{item.name}")
    outcome = yield     # 将控制权交回给pytest,运行测试用例
    # 在测试用例之后执行
    result = outcome.get_result()
    print(f"end test case:{item.name},test result{result.outcome}")
  
@pytest.fixture()
def logstatus():
    print("login")
    yield
    print("logout")
# 测试用例

import pytest
@pytest.mark.usefixtures("logstatus")
class Test_demo:
    def test_case1(self):
        print("execute test case 1:")
        assert 1+1==3

    def test_case2(self):
        print("execute test case 2:")
        assert 1+8==7

    def test_case3(self):
        print("execute test case 3:")
        assert 99+1==100
# 结果打印(部分)
test_example.py start run test case:test_case1
end test case:test_case1,test resultpassed
start run test case:test_case1
end test case:test_case1,test resultfailed
Fstart run test case:test_case1
end test case:test_case1,test resultpassed
start run test case:test_case2
end test case:test_case2,test resultpassed
start run test case:test_case2
end test case:test_case2,test resultfailed
Fstart run test case:test_case2
end test case:test_case2,test resultpassed
start run test case:test_case3
end test case:test_case3,test resultpassed
start run test case:test_case3
end test case:test_case3,test resultpassed
.start run test case:test_case3
end test case:test_case3,test resultpassed

配置pytest的默认行为和标记等,简化命令行输入

  • 依靠pytest.ini实现配置。
  • 自定义标记,如需要标记一个smoke测试,标记完之后只需要输入pytest -m smoke即可执行所有标记为smoke的测试用例:
# 在pytest.ini中标记
[pytest]
markers =
    smoke: 标记冒烟测试用例
    regression: 标记回归测试用例
    interface: 标记接口测试用例
    ui: 标记 UI 测试用例
# 只运行标记为 smoke 的测试用例
pytest -m smoke  
  • 配置命令行参数,避免每次手动输入:
[pytest]
addopts =
    --setup-show       # 显示测试用例的 setup 和 teardown 过程
    -v                 # 显示详细的测试用例信息
    -s                 # 禁用输出捕获,直接显示 print/logging 输出
    --junitxml=report.xml  # 生成 JUnit 格式的测试报告
    --html=report.html     # 生成 HTML 格式的测试报告
    --self-contained-html  # 生成独立的 HTML 报告
    --tb=short             # 显示简短的回溯信息
    --capture=tee-sys  # 同时将日志输出到终端和文件
    --cov=src             # 统计 src 目录的代码覆盖率
    --cov-report=html     # 生成 HTML 格式的覆盖率报告
    --cov-report=term     # 在终端显示覆盖率报告

如何让git忽略某一些文件(比如log,report这一类)

  • 如何实现.gitignore:
    • 在项目的根目录下创建一个名为 .gitignore 的文件;
# 忽略 IDE 配置文件
.vscode/
.idea/
*.suo
*.user

# 忽略 Python 文件
*.pyc
*.pyo
__pycache__/

# 忽略虚拟环境
.venv/
env/
venv/

# 忽略日志文件
*.log

# 忽略测试报告
report/
output/

# 忽略敏感配置文件
config.yml
.env
  • 已被跟踪的文件不会被忽略:
    • 解决办法:git rm --cached <file>
  • 检查某文件是否被忽略
    • git check ignore -v <file>

在测试项目中,依靠poetry完成版本锁定和一致性管理

  • 安装poetry,pip install poetry;
  • 检查安装状态,poetry --version;
  • 初始化项目,poetry init;
  • 创建虚拟环境并安装依赖,poetry install;
  • 如何使用poetry管理依赖:
    • Poetry 会在项目根目录下生成 pyproject.toml 和 poetry.lock 文件:
      • pyproject.toml:定义项目的依赖及其版本范围;
      • poetry.lock:锁定依赖的具体版本,确保团队成员或 CI/CD 环境中使用的依赖版本一致。
    • 根据pyproject.toml中的版本范围,使用指令poetry update更新所有依赖并重新生成poetry.lock
  • 以下是实现依赖的版本锁定和一致性管理关键步骤:
    • 使用 poetry update 和 poetry install 确保依赖一致。
    • 在 pyproject.toml 中明确指定版本范围。
    • 使用 poetry.lock 文件锁定具体版本,避免版本不一致。
    • 在团队中共享 pyproject.toml 和 poetry.lock 文件,确保所有环境一致。

使用pytest写case的时候常用到的一些OOP知识点

  • __init__:类的构造方法,在创建类的实例时自动调用,用于初始化实例的属性或执行其他必要的设置。
    • __init__ 方法的第一个参数必须是self,表示实例本身。
    • 不需要显式调用,实例化对象时会自动执行.
class MyClass:
    def __init__(self, name, age):
        self.name = name  # 初始化实例属性
        self.age = age

# 创建实例时自动调用 __init__
obj = MyClass("Alice", 25)
print(obj.name)  # 输出: Alice
print(obj.age)   # 输出: 25
  • self:是实例方法的第一个参数,表示类的实例本身。
    • 在类的方法中,必须显式传递self,以便访问实例的属性和方法.
class MyClass:
    def __init__(self, name):
        self.name = name

    def greet(self):
        print(f"Hello, {self.name}!")  # 通过 self 访问实例属性

obj = MyClass("Bob")
obj.greet()  # 输出: Hello, Bob!
  • cls:是类方法的第一个参数,表示类本身;
    • 类方法使用@classmethod装饰器定义;
class MyClass:
    class_variable = "I am a class variable"

    @classmethod
    def show_class_variable(cls):
        print(cls.class_variable)  # 通过 cls 访问类属性

MyClass.show_class_variable()  # 输出: I am a class variable
  • @classmethod:类方法,使用@classmethod装饰器定义,接收cls参数,表示类本身;
    • 类方法可以被继承
    • 类方法可以被子类重写,在重写的类方法中,cls会指向子类本身,从而实现动态行为.
class Parent:
    class_variable = "Parent Class"

    @classmethod
    def show_class_info(cls):
        return f"This is {cls.class_variable}"


class Child(Parent):
    class_variable = "Child Class"

    @classmethod
    def show_class_info(cls):
        return f"This is {cls.class_variable}, overridden in Child"


# 调用父类的类方法
print(Parent.show_class_info())  # 输出: This is Parent Class

# 调用子类的重写类方法
print(Child.show_class_info())   # 输出: This is Child Class, overridden in Child
  • @staticmethod,静态方法:
class Math:
    @staticmethod
    def add(a, b):
        return a + b

print(Math.add(3, 5))  # 输出: 8
posted @ 2025-04-17 17:14  你要去码头整点薯条吗  阅读(63)  评论(0)    收藏  举报