pytest笔记
安装pytest
- 安装
pip install -U pytest
- 查验
# 查看version
pytest --version
# 查看包的安装位置
pip list
pip show pytest
- 第一个测试用例
- 新建py文件,文件名需要
test_开头,如test_sample.py; - 使用断言
assert; - 用例完成后,将执行路径cd当当前文件所在路径下,然后终端输入
pytest执行该用例;
- 新建py文件,文件名需要
# 测试用例如下
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的基本运行机制:程序运行时不需要编译成二进制代码,而直接从源码运行程序,简单说来就是python解释器把源码变成字节码,再由解释器来执行这些字节码。第一次执行代码的时候,python解释器把已经编译过把字节码放在
- 从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")覆盖整个执行过程;
- function:
-
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 会在项目根目录下生成 pyproject.toml 和 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

浙公网安备 33010602011771号