pytest总结
*文档正在更新中---2021/10/21 Auth:Fanjiexiong* 添加了setup_method的和setup不同之处
pytest简介
pytest是python的一种单元测试框架,与python自带的unittest测试框架类似,但是比unittest框架使用起来更简洁,效率更高。根据pytest的官方网站介绍,它具有如下特点:
- 非常容易上手,入门简单,文档丰富,文档中有很多实例可以参考
- 能够支持简单的单元测试和复杂的功能测试
- 支持参数化
- 执行测试过程中可以将某些测试跳过(skip),或者对某些预期失败的case标记成失败
- 支持重复执行(rerun)失败的case
- 支持运行由nose, unittest编写的测试case
- 可生成html报告
- 方便的和持续集成工具jenkins集成
- 可支持执行部分用例
- 具有很多第三方插件,并且可以自定义扩展
安装pytest
pip3 install pytest
快速开始
1.新建一个test_sample.py文件,写以下代码
# content of test_sample.py
def func(x):
return x +1
def test_answer():
assert func(3)==5
2.打开test_sample.py所在的文件夹,cmd窗口输入:pytest test_sample.py(可以自己检索以test开头的文件夹)
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:36:59] C:2
$ pytest test_sample.py
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 1 item
test_sample.py F [100%]
============================================================================== FAILURES ===============================================================================
_____________________________________________________________________________ test_answer _____________________________________________________________________________
def test_answer():
> assert func(3)==5
E assert 4 == 5
E + where 4 = func(3)
test_sample.py:14: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_sample.py::test_answer - assert 4 == 5
========================================================================== 1 failed in 0.06s ==========================================================================
写个测试类
1.前面是写的一个test开头的测试函数,当用例用多个的时候,写函数就不太合适了。这时可以把多个测试用例,写到一个测试类里。
class TestClass:
def test_one(self):
x = "this"
assert 'h' in x
def test_two(self):
x = "hello"
assert hasattr(x, 'check')
2.pytest会找到符合规则(test_.py和_test.py)所有测试,因此它发现两个test_前缀功能。 如果只想运行其中一个,可以指定传递文件名test_class.py来运行模块:
备注: -q, --quiet decrease verbosity( 显示简单结果)
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:37:31] C:1
$ pytest test_sample.py -q
.F [100%]
============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestClass.test_two __________________________________________________________________________
self = <test_sample.TestClass object at 0x7f4de8c27240>
def test_two(self):
x = "hello"
> assert hasattr(x, 'check')
E AssertionError: assert False
E + where False = hasattr('hello', 'check')
test_sample.py:17: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_sample.py::TestClass::test_two - AssertionError: assert False
1 failed, 1 passed in 0.05s
多种方式执行用例
按关键字模糊匹配你要执行的测试用例
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:43:55]
$ pytest -k 'two'
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 2 items / 1 deselected / 1 selected
test_sample.py F [100%]
============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestClass.test_two __________________________________________________________________________
self = <test_sample.TestClass object at 0x7f8637ce4e80>
def test_two(self):
x = "hello"
> assert hasattr(x, 'check')
E AssertionError: assert False
E + where False = hasattr('hello', 'check')
test_sample.py:17: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_sample.py::TestClass::test_two - AssertionError: assert False
=================================================================== 1 failed, 1 deselected in 0.05s ===================================================================
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:39:10] C:1
$ pytest -k 'one'
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 2 items / 1 deselected / 1 selected
test_sample.py . [100%]
=================================================================== 1 passed, 1 deselected in 0.01s ===================================================================
按节点运行
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:44:16] C:1
$ pytest test_sample.py::TestClass::test_one
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 1 item
test_sample.py . [100%]
========================================================================== 1 passed in 0.01s ==========================================================================
根据标记mark运行
import pytest
class TestClass:
@pytest.mark.one
def test_one(self):
x = "this"
assert 'h' in x
def test_two(self):
x = "hello"
assert hasattr(x, 'check')
运行:
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [10:47:23]
$ pytest -m one
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 2 items / 1 deselected / 1 selected
test_sample.py . [100%]
========================================================================== warnings summary ===========================================================================
test_sample.py:11
/home/tarzan/code/activatorweb-bug-to-autotest/test_sample.py:11: PytestUnknownMarkWarning: Unknown pytest.mark.one - is this a typo? You can register custom marks to avoid this warning - for details, see https://docs.pytest.org/en/stable/mark.html
@pytest.mark.one
-- Docs: https://docs.pytest.org/en/stable/warnings.html
============================================================= 1 passed, 1 deselected, 1 warning in 0.02s ==============================================================
解决:You can register custom marks to avoid this warning
新建一个pytest.ini文件写入,格式不要变
[pytest]
markers =
one
two
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [11:05:39]
$ pytest -m one
========================================================================= test session starts =========================================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collected 2 items / 1 deselected / 1 selected
test_sample.py . [100%]
=================================================================== 1 passed, 1 deselected in 0.01s ===================================================================
遇到错误停止运行
pytest -x test_class.py 遇到错误就停止
pytest --maxfail=2 最大遇到两个失败的就停止
使用python test_*.py方式运行
import pytest
class TestClass:
def test_one(self):
x = "this"
assert 'h' in x
def test_two(self):
x = "hello"
assert hasattr(x, 'check')
def test_three(self):
a = "hello"
b = "hello world"
assert a in b
if __name__ == "__main__":
pytest.main(['-q', 'test_sample.py'])
运行:
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [11:12:02] C:1
$ python3 test_sample.py
.F. [100%]
============================================================================== FAILURES ===============================================================================
_________________________________________________________________________ TestClass.test_two __________________________________________________________________________
self = <test_sample.TestClass object at 0x7fad4f2204e0>
def test_two(self):
x = "hello"
> assert hasattr(x, 'check')
E AssertionError: assert False
E + where False = hasattr('hello', 'check')
test_sample.py:17: AssertionError
======================================================================= short test summary info =======================================================================
FAILED test_sample.py::TestClass::test_two - AssertionError: assert False
1 failed, 2 passed in 0.04s
测试用例setup和teardown
- 模块级(setup_module/teardown_module)开始于模块始末,全局的
- 函数级(setup_function/teardown_function)只对函数用例生效(不在类中)
- 类级(setup_class/teardown_class)只在类中前后运行一次(在类中)
- 方法级(setup_method/teardown_method)开始于方法始末(在类中)
- 类里面的(setup/teardown)运行在调用方法的前后
setup_function:每个测试用例前后执行。用来debug调试只有你失败的时候setup_function 的内容打印如果你用例通过他是不会显示的(终端运行)
import pytest
def setup_function():
print("setup_function:每个用例开始前都会执行")
def teardown_function():
print("teardown_function:每个用例结束后都会执行")
def test_one():
print("正在执行----test_one")
x = "this"
assert 'h' in x
def test_two():
print("正在执行----test_two")
x = "hello"
assert hasattr(x, 'check')
def test_three():
print("正在执行----test_three")
a = "hello"
b = "hello world"
assert a in b
if __name__ == "__main__":
pytest.main(['-q', 'test_sample.py'])
运行:
# tarzan @ tarzan3w in ~/code/activatorweb-bug-to-autotest on git:master x [11:23:56]
$ python3 test_sample.py -q
.F. [100%]
============================================================================== FAILURES ===============================================================================
______________________________________________________________________________ test_two _______________________________________________________________________________
def test_two():
print("正在执行----test_two")
x = "hello"
> assert hasattr(x, 'check')
E AssertionError: assert False
E + where False = hasattr('hello', 'check')
test_sample.py:24: AssertionError
------------------------------------------------------------------------ Captured stdout setup ------------------------------------------------------------------------
setup_function:每个用例开始前都会执行
------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------
正在执行----test_two
---------------------------------------------------------------------- Captured stdout teardown -----------------------------------------------------------------------
teardown_function:每个用例结束后都会执行
======================================================================= short test summary info =======================================================================
FAILED test_sample.py::test_two - AssertionError: assert False
1 failed, 2 passed in 0.04s
注意:如果是在终端的话不会有很多的详细日志,但是如果在pycharm中会显示所有的print
setup_module:每一个py文件之运行一次
不用多说/如果是在终端使用python test_.py话不会有很多的详细日志,但是如果在pycharm中会显示所有的print
在类中使用setup:
import pytest
class TestCase():
def setup(self):
print("setup: 每个用例开始前执行")
def teardown(self):
print("teardown: 每个用例结束后执行")
def setup_class(self):
print("setup_class:所有用例执行之前")
def teardown_class(self):
print("teardown_class:所有用例执行之前")
def setup_method(self):
print("setup_method: 每个用例开始前执行")
def teardown_method(self):
print("teardown_method: 每个用例结束后执行")
def test_one(self):
print("正在执行----test_one")
x = "this"
assert 'h' in x
def test_two(self):
print("正在执行----test_two")
x = "hello"
assert hasattr(x, 'check')
def test_three(self):
print("正在执行----test_three")
a = "hello"
b = "hello world"
assert a in b
if __name__ == "__main__":
pytest.main(['-q', 'test_sample.py'])
pycharm运行:
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 3 items
test_sample.py::TestCase::test_one setup_class:所有用例执行之前
setup_method: 每个用例开始前执行
setup: 每个用例开始前执行
PASSED [ 33%]正在执行----test_one
teardown: 每个用例结束后执行
teardown_method: 每个用例结束后执行
test_sample.py::TestCase::test_two setup_method: 每个用例开始前执行
setup: 每个用例开始前执行
FAILED [ 66%]正在执行----test_two
test_sample.py:34 (TestCase.test_two)
self = <test_sample.TestCase object at 0x7fc4a975bb00>
def test_two(self):
print("正在执行----test_two")
x = "hello"
> assert hasattr(x, 'check')
E AssertionError: assert False
E + where False = hasattr('hello', 'check')
test_sample.py:38: AssertionError
teardown: 每个用例结束后执行
teardown_method: 每个用例结束后执行
test_sample.py::TestCase::test_three setup_method: 每个用例开始前执行
setup: 每个用例开始前执行
PASSED [100%]正在执行----test_three
teardown: 每个用例结束后执行
teardown_method: 每个用例结束后执行
teardown_class:所有用例执行之前
=================================== FAILURES ===================================
______________________________ TestCase.test_two _______________________________
self = <test_sample.TestCase object at 0x7fc4a975bb00>
def test_two(self):
print("正在执行----test_two")
x = "hello"
> assert hasattr(x, 'check')
E AssertionError: assert False
E + where False = hasattr('hello', 'check')
test_sample.py:38: AssertionError
---------------------------- Captured stdout setup -----------------------------
setup_method: 每个用例开始前执行
setup: 每个用例开始前执行
----------------------------- Captured stdout call -----------------------------
正在执行----test_two
--------------------------- Captured stdout teardown ---------------------------
teardown: 每个用例结束后执行
teardown_method: 每个用例结束后执行
=========================== short test summary info ============================
FAILED test_sample.py::TestCase::test_two - AssertionError: assert False
========================= 1 failed, 2 passed in 0.02s ==========================
Process finished with exit code 1
Assertion failed
Assertion failed
Assertion failed
从结果看出,运行的优先级:setup_class》setup_method》setup 》用例》teardown》teardown_method》teardown_class
setup和setup_method的不同之处
setup和setup_method不同之处,我查了一下大概是Pytest中的setup和teardown是为了支持unittest中的setUp所遗留的,我建议在Pytest中使用setup_method(self, method),这样你就可以查看当前的执行的测试用例名称了
==================
class TestCMD(object):
def setup_class(self):
pass
def teardown_class(self):
pass
def setup_method(self, method):
print("\n%s:%s" % (type(self).__name__, method.__name__))
print(method.__doc__)
pass
def teardown_method(self, method):
pass
def test_cmd(self):
"""测试标题:输入正确的序列号能够正常激活"""
assert "foo" != "bar"
函数和类混合
1.如果一个.py的文件里面既有函数用例又有类和方法用例,运行顺序又是怎样的呢?
# coding:utf-8
import pytest
def setup_module():
print("setup_module:整个.py模块只执行一次")
print("比如:所有用例开始前只打开一次浏览器")
def teardown_module():
print("teardown_module:整个.py模块只执行一次")
print("比如:所有用例结束只最后关闭浏览器")
def setup_function():
print("setup_function:每个用例开始前都会执行")
def teardown_function():
print("teardown_function:每个用例结束前都会执行")
def test_one():
print("正在执行----test_one")
x = "this"
assert 'h' in x
def test_two():
print("正在执行----test_two")
x = "hello"
assert hasattr(x, 'check')
class TestCase():
def setup_class(self):
print("setup_class:所有用例执行之前")
def teardown_class(self):
print("teardown_class:所有用例执行之前")
def test_three(self):
print("正在执行----test_three")
x = "this"
assert 'h' in x
def test_four(self):
print("正在执行----test_four")
x = "hello"
assert hasattr(x, 'check')
if __name__ == "__main__":
pytest.main(["-s", "test_fixtclass.py"])
运行结果:
test_fixtclass.py setup_module:整个.py模块只执行一次
比如:所有用例开始前只打开一次浏览器
setup_function:每个用例开始前都会执行
正在执行----test_one
.teardown_function:每个用例结束前都会执行
setup_function:每个用例开始前都会执行
正在执行----test_two
Fteardown_function:每个用例结束前都会执行
setup_class:所有用例执行之前
正在执行----test_three
.正在执行----test_four
Fteardown_class:所有用例执行之前
teardown_module:整个.py模块只执行一次
比如:所有用例结束只最后关闭浏览器
2.从运行结果看出,setup_module/teardown_module的优先级是最大的,然后函数里面用到的setup_function/teardown_function与类里面的setup_class/teardown_class互不干涉
跳过测试用例
skip
import pytest
class TestClass:
@pytest.mark.one
def test_one(self):
x = "this"
assert 'h' in x
@pytest.mark.skip(reason="no way of currently testing this")
def test_two(self):
x = "hello"
assert 'h' not in x
if __name__ == '__main__':
pytest.main(['test_test.py'])
运行后:
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items
test_test.py::TestClass::test_one PASSED [ 50%]
test_test.py::TestClass::test_two SKIPPED (no way of currently testi...) [100%]
Skipped: no way of currently testing this
skipif
import pytest
class TestClass:
@pytest.mark.one
def test_one(self):
x = "this"
assert 'h' in x
@pytest.mark.skipif(2>1, reason='如果2大于1了,才跳过')
def test_two(self):
x = "hello"
assert 'h' not in x
if __name__ == '__main__':
pytest.main(['test_test.py'])
把skipif当成一个变量,其他的py文件也可以调用
import pytest
myskip = pytest.mark.skipif(1>0,reason='1大于0的话,就跳过测试用例')
class TestClass:
@pytest.mark.one
def test_one(self):
x = "this"
assert 'h' in x
@myskip
def test_two(self):
x = "hello"
assert 'h' not in x
if __name__ == '__main__':
pytest.main(['test_test.py'])
执行结果:
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items
test_test.py::TestClass::test_one PASSED [ 50%]
test_test.py::TestClass::test_two SKIPPED (1大于0的话,就跳过测试用例) [100%]
Skipped: 1大于0的话,就跳过测试用例
函数传参和fixture传参数request
前言
为了提高代码的复用性,我们在写用例的时候,会用到函数,然后不同的用例去调用这个函数。
比如登录操作,大部分的用例都会先登录,那就需要把登录单独抽出来写个函数,其它用例全部的调用这个登陆函数就行。
但是登录的账号不能写死,有时候我想用账号1去登录,执行用例1,用账号2去登录执行用例2,所以需要对函数传参。
fixture
import pytest
@pytest.fixture
def error_fixture():
print('这个是setup中要运行的error_fixture')
assert 0
def test_error(error_fixture):
print('test_error')
pass
if __name__ == "__main__":
pytest.main(["-s", "test_test.py"])
运行:
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 1 item
test_test.py::test_error ERROR [100%]这个是setup中要运行的error_fixture
test setup failed
@pytest.fixture
def error_fixture():
print('这个是setup中要运行的error_fixture')
> assert 0
E assert 0
test_test.py:6: AssertionError
==================================== ERRORS ====================================
_________________________ ERROR at setup of test_error _________________________
@pytest.fixture
def error_fixture():
print('这个是setup中要运行的error_fixture')
> assert 0
E assert 0
test_test.py:6: AssertionError
---------------------------- Captured stdout setup -----------------------------
这个是setup中要运行的error_fixture
=========================== short test summary info ============================
ERROR test_test.py::test_error - assert 0
=============================== 1 error in 0.01s ===============================
登录函数传参
把登录单独出来,写一个函数,传2个参数user和psw,写用例的时候调用登录函数,输入几组user,psw参数化登录用例
测试用例传参需要用装饰器@pytest.mark.parametrize,里面写两个参数
- 第一个参数是字符串,多个参数中间用逗号隔开
- 第二个参数是list,多组数据用元祖类型
import pytest
test_login_data = [("admin", "111111"), ("admin", "")]
def login(user, psw):
'''普通登录函数'''
print("登录账户:%s"%user)
print("登录密码:%s"%psw)
if psw:
return True
else:
return False
@pytest.mark.parametrize("user, psw", test_login_data)
def test_login(user, psw):
'''登录用例'''
result = login(user, psw)
assert result == True, "失败原因:密码为空"
if __name__ == "__main__":
pytest.main(["-s", "test_test.py"])
执行结果
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items
test_test.py::test_login[admin-111111] PASSED [ 50%]登录账户:admin
登录密码:111111
test_test.py::test_login[admin-] FAILED [100%]登录账户:admin
登录密码:
AssertionError: 失败原因:密码为空
False != True
Expected :True
Actual :False
<Click to see difference>
user = 'admin', psw = ''
@pytest.mark.parametrize("user, psw", test_login_data)
def test_login(user, psw):
'''登录用例'''
result = login(user, psw)
> assert result == True, "失败原因:密码为空"
E AssertionError: 失败原因:密码为空
E assert False == True
test_test.py:20: AssertionError
=================================== FAILURES ===================================
______________________________ test_login[admin-] ______________________________
user = 'admin', psw = ''
@pytest.mark.parametrize("user, psw", test_login_data)
def test_login(user, psw):
'''登录用例'''
result = login(user, psw)
> assert result == True, "失败原因:密码为空"
E AssertionError: 失败原因:密码为空
E assert False == True
test_test.py:20: AssertionError
----------------------------- Captured stdout call -----------------------------
登录账户:admin
登录密码:
=========================== short test summary info ============================
FAILED test_test.py::test_login[admin-] - AssertionError: 失败原因:密码为空
========================= 1 failed, 1 passed in 0.02s ==========================
request参数
如果想把登录操作放到前置操作里,也就是用到@pytest.fixture装饰器,传参就用默认的request参数
user = request.param 这一步是接收传入的参数,本案例是传一个参数情况
import pytest
test_user_data = ["admin1", "admin2"]
i = 0
@pytest.fixture(scope="module")
def login(request):
user = request.param
global i
i += 1
print('这个会打印两次,用i的值记录i=:%s'%i)
print("登录账户:%s"%user)
return user
@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
'''登录用例'''
a = login
print("测试用例中login的返回值:%s" % a)
assert a != ""
if __name__ == "__main__":
pytest.main(["-s", "test_test.py"])
执行后的:
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items
test_test.py::test_login[admin1] 这个会打印两次,用i的值记录i=:1
登录账户:admin1
PASSED [ 50%]测试用例中login的返回值:admin1
test_test.py::test_login[admin2] 这个会打印两次,用i的值记录i=:2
登录账户:admin2
PASSED [100%]测试用例中login的返回值:admin2
============================== 2 passed in 0.01s ===============================
request传2个参数
如果用到@pytest.fixture,里面用2个参数情况,可以把多个参数用一个字典去存储,这样最终还是只传一个参数
不同的参数再从字典里面取对应key值就行,如: user = request.param["user"]
import pytest
test_user_data = [{"user": "admin1", "psw": "111111"},
{"user": "admin1", "psw": ""}]
@pytest.fixture(scope="module")
def login(request):
user = request.param["user"]
psw = request.param["psw"]
print("登录账户:%s" % user)
print("登录密码:%s" % psw)
if psw:
return True
else:
return False
# indirect=True 声明login是个函数
@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
'''登录用例'''
a = login
print("测试用例中login的返回值:%s" % a)
assert a, "失败原因:密码为空"
if __name__ == "__main__":
pytest.main(["-s", "test_test.py"])
执行后的:
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 2 items
test_test.py::test_login[login0] 登录账户:admin1
登录密码:111111
PASSED [ 50%]测试用例中login的返回值:True
test_test.py::test_login[login1] 登录账户:admin1
登录密码:
FAILED [100%]测试用例中login的返回值:False
test_test.py:20 (test_login[login1])
login = False
@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
'''登录用例'''
a = login
print("测试用例中login的返回值:%s" % a)
> assert a, "失败原因:密码为空"
E AssertionError: 失败原因:密码为空
E assert False
test_test.py:26: AssertionError
=================================== FAILURES ===================================
______________________________ test_login[login1] ______________________________
login = False
@pytest.mark.parametrize("login", test_user_data, indirect=True)
def test_login(login):
'''登录用例'''
a = login
print("测试用例中login的返回值:%s" % a)
> assert a, "失败原因:密码为空"
E AssertionError: 失败原因:密码为空
E assert False
test_test.py:26: AssertionError
---------------------------- Captured stdout setup -----------------------------
登录账户:admin1
登录密码:
----------------------------- Captured stdout call -----------------------------
测试用例中login的返回值:False
=========================== short test summary info ============================
FAILED test_test.py::test_login[login1] - AssertionError: 失败原因:密码为空
========================= 1 failed, 1 passed in 0.04s ==========================
pytest框架之fixture前置和后置
一、conftest.py
- 定义公共的fixture,多个测试类中都可以调用
- pytest提供了conftest.py文件,可以将fixture定义在此文件中
- 运行测试用例时,不需要去导入这个文件,会自动去查找conftest.py文件,然后去找到对用的fixture
代码实现:
conftest.py:
import pytest
@pytest.fixture()
def fun_():
print('自动找到conftest文件导入fun_')
print('这个是前置')
a = 1
yield a
print('这个是后置')
test_test.py:
import pytest
class TestFun(object):
def test_123(self, fun_):
print('用例执行')
if __name__ == "__main__":
pytest.main(["-s", "test_test.py"])
运行结果:
============================= test session starts ==============================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/tarzan/code/activatorweb-bug-to-autotest, configfile: pytest.ini
plugins: allure-pytest-2.9.43
collecting ... collected 1 item
test_test.py::TestFun::test_123 自动找到conftest文件导入fun_
这个是前置
PASSED [100%]用例执行
这个是后置
============================== 1 passed in 0.01s ===============================
二、前置和后置
fixture函数根据关键字yield作为前置和后置的分割线,并且yield也可以接收返回值,作用相当于return
import pytest
@pytest.fixture()
def init_demo():
print("这是测试用例的前置")
a = 1
yield a # 分割线(yield + 返回值)
print("这是测试用例的后置")
三、用fixture装饰器调用fixture
在测试用例/测试类前面加上@pytest.mark.usefixtures('fixture函数名称')
ps:定义conftest.py文件,在此文件中可以定义多个fixture,pytest会自动搜索此文件
test_test.py: 测试类加上装饰器
import pytest
@pytest.mark.usefixtures('fun_')
class TestFun(object):
def test_123(self):
print('用例执行')
if __name__ == "__main__":
pytest.main(["-s", "test_test.py"])
test_test.py: 测试用例加上装饰器
import pytest
class TestFun(object):
@pytest.mark.usefixtures('fun_')
def test_123(self):
print('用例执行')
if __name__ == "__main__":
pytest.main(["-s", "test_test.py"])
生成HTML报告
# 安装pytest-html插件
pip3 install pytest-html
# 执行命令生成报告
pytest --html=report.html
# 指定到对应的文件夹中
pytest --html=report/report.html # 这里不要加 .
如果我们想从app_main运行测试用例呢!!!
app_main.py
import pytest
import shutil
from pathlib import Path
if __name__ == '__main__':
report_path = Path.cwd() / 'report'
if report_path.exists():
shutil.rmtree(report_path)
report_path.mkdir()
pytest.main(["--html", "report/report.html"]) # 这里就是把命令拆成列表中的元素
根据allure服务器生成网页版报告
# 安装allure-pytest
pip3 install allure-pytest --index-url https://pypi.douban.com/simple # 请先卸载掉 pytest-allure-adaptor
# 安装allure启动一个服务来读取报告
https://github.com/allure-framework/allure2/releases # 我这里安装的2.14 直接下载deb包然后安装
# 执行用例生成后会生成原始文件.json这个只是测试报告的原始文件,不能打开成html的报告
pytest --alluredir reults/
# 执行完成后pytest --alluredir report/,report目录会生成一个allure_raw的原始文件,这个只是测试报告的原始文件,不能打开成html的报告
# 这个时候需要启动allure服务器来读取对应的原始文件,
sudo allure serve reults/
allure serve report/
$ sudo allure serve report/
请输入密码
[sudo] tarzan 的密码:
验证成功
Generating report to temp directory...
Report successfully generated to /tmp/11808150245710901367/allure-report
Starting web server...
2021-08-19 10:58:24.553:INFO::main: Logging initialized @1691ms to org.eclipse.jetty.util.log.StdErrLog
Browse operation is not supported on your platform.You can use the link below to open the report manually.
java.lang.UnsupportedOperationException: The BROWSE action is not supported on the current platform!
at java.desktop/java.awt.Desktop.checkActionSupport(Desktop.java:380)
at java.desktop/java.awt.Desktop.browse(Desktop.java:524)
at io.qameta.allure.Commands.openBrowser(Commands.java:222)
at io.qameta.allure.Commands.open(Commands.java:152)
at io.qameta.allure.Commands.serve(Commands.java:136)
at io.qameta.allure.CommandLine.run(CommandLine.java:159)
at java.base/java.util.Optional.orElseGet(Optional.java:369)
at io.qameta.allure.CommandLine.main(CommandLine.java:88)
Server started at <http://127.0.1.1:46013/>. Press <Ctrl+C> to exit
然后访问这个链接就好了: http://127.0.1.1:46013/ 端口是随机的
如果我们想从app_main运行测试用例呢!!!
app_main.py
if __name__ == '__main__':
report_path = Path.cwd() / 'report'
if report_path.exists():
shutil.rmtree(report_path)
report_path.mkdir()
results_path = Path.cwd() / 'results'
if results_path.exists():
shutil.rmtree(results_path)
results_path.mkdir()
pytest.main(["--html", "report/report.html", '--alluredir', 'results/'])
生成详细的测试报告(并且是动态修改描述信息-TODO)
Allure提供了以下常用注解(未列出部分请访问官网了解),具体用法如下。
-
@Epic
敏捷的术语,定义史诗,往下再分Feature和Story。 -
@Feature
敏捷的术语,定义功能模块,往下是Story。 -
@Story
敏捷的术语,定义用户故事。 -
@Title
定义用例名称。 -
@Description
定义用例描述。 -
@Issue
定义缺陷链接。可结合@Link使用,也可以结合配置文件使用。配置文件放到resource目录下,Allure 会替换{}为对应注解的值。 -
@TmsLink
与@Issue类似用法,定义案例链接。@Link
定义一个链接,在测试报告展现。@Severity
定义用例的级别,主要有BLOCKER,CRITICAL,MINOR,NORMAL,TRIVIAL等几种类型,默认是NORMAL。
@allure.epic("项目名称: 授权管理") # 定义项目
@allure.severity("blocker") # 用例等级 blocker 堵塞 、critical至关重要的 、normal 正常 、minor 轻微 、trivial微不足道的
@allure.issue("https://pms.uniontech.com/bug-view-115439.html") # 禅道bug地址
@allure.testcase("http://149.335.82.12:8080/zentao/testcase-view-5-1.html") # 禅道用例连接地址
@allure.feature("CMD-更换序列号")
def test_3():
print('执行了服务器欧拉版特性3用例')
logging.info('第一步:输入下载序列号')
logging.info('第二步:序列号激活')
logging.info('第三步:更换序列号')
logging.info('第四步:清理测试环境')
assert False
@allure.epic("项目名称: 授权管理") # 定义项目
@allure.severity("blocker") # 用例等级 blocker 堵塞 、critical至关重要的 、normal 正常 、minor 轻微 、trivial微不足道的
@allure.issue("https://pms.uniontech.com/bug-view-115439.html") # 禅道bug地址
@allure.testcase("http://149.335.82.12:8080/zentao/testcase-view-5-1.html") # 禅道用例连接地址
@allure.feature("CMD-更换序列号")
@pytest.mark.core
def test_1(setup):
"""
测试场景是;
"""
var = os.environ.get('PYTEST_CURRENT_TEST')
print('+++++++++++++++'+var)
var = pytest.request._pyfuncitem._obj.__doc__
print(var)
with allure.step('第一步:输入下载序列号'):
logger.info('第一步成功')
with allure.step('第二步:序列号激活'):
logger.info('第二步成功')
with allure.step('第三步:更换序列号'):
logger.error('第三步失败')
assert False
您可以为测试添加详细描述,以便根据需要向报告阅读器提供尽可能多的上下文
import allure
@allure.description_html("""
<h1>Test with some complicated html description</h1>
<table style="width:100%">
<tr>
<th>Firstname</th>
<th>Lastname</th>
<th>Age</th>
</tr>
<tr align="center">
<td>William</td>
<td>Smith</td>
<td>50</td>
</tr>
<tr align="center">
<td>Vasya</td>
<td>Jackson</td>
<td>94</td>
</tr>
</table>
""")
def test_html_description():
assert True
@allure.title
使用特殊的装饰器可以使测试标题更具可读性。标题支持参数的占位符并支持动态替换。
import allure
import pytest
@allure.title("This test has a custom title")
def test_with_a_title():
assert 2 + 2 == 4
@allure.title("This test has a custom title with unicode: Привет!")
def test_with_unicode_title():
assert 3 + 3 == 6
@allure.title("Parameterized test title: adding {param1} with {param2}")
@pytest.mark.parametrize('param1,param2,expected', [
(2, 2, 4),
(1, 2, 5)
])
def test_with_parameterized_title(param1, param2, expected):
assert param1 + param2 == expected
@allure.title("This title will be replaced in a test body")
def test_with_dynamic_title():
assert 2 + 2 == 4
allure.dynamic.title('After a successful test finish, the title was replaced with this line.')
失败重跑机制
当1000个用例中有一个用例因为网络波动导致了失败,那么我们需要添加重跑机制,比如等待10s后重新执行一次
开始教程:我们首先安装一个小插件
pip3 install pytest-rerunfailures -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
python文件中添加这个命令
pytest.main(["--html", "report/report.html", '--alluredir', 'results/', '--reruns', '2', '--reruns-delay', '10'])
命令行运行:
pytest test_case/test_product.py --reruns 2 --reruns-delay 10
针对单个用例使用装饰器来运行:
@pytest.mark.flaky(reruns=5, reruns_delay=2)
def test_case_1(self):
"""
查询软件工具产品-返回所有软件工具产品
:return:
"""
url = base_url + WebApi.software_list + '?' + 'id=&product_id=&status=&purpose_type=2&start_date=&end_date=&offset=0&limit=10'
logger.info('获取的url' + url)
headers = {'authtoken': self.token}
res = requests.get(url, headers=headers)
logger.info(res.text)
for x in res.json().get('rows'):
assert x.get('product_type_cn') == "软件工具所属类型1"
运行上次失败用例
“80%的bug集中在20%的模块,越是容易出现bug的模块,bug是越改越多“平常我们做手工测试的时候,比如用100个用例需要执行,其中10个用例失败了,
当开发修复完bug后,我们一般是重点测上次失败的用例。
那么自动化测试也一样,当用例特别多时,为了节省时间,第一次部分用例失败了,修复完之后,可以只测上次失败的用例。
--lf, --last-failed 只重新运行上次运行失败的用例(或如果没有失败的话会全部跑)
--ff, --failed-first 运行所有测试,但首先运行上次运行失败的测试(这可能会重新测试,从而导致重复的fixture setup/teardown)
代码实现:test_1.py
import pytest
def test_case_1():
assert 1 != 1
def test_case_2():
assert 1 == 1
if __name__ == '__main__':
pytest.main(['-s'])
在终端运行:
pytest test_1.py
结果:
$ pytest test_1.py
======================================================= test session starts ========================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/Desktop
plugins: allure-pytest-2.8.6, html-3.1.1, metadata-1.11.0, rerunfailures-10.1
collected 2 items
test_1.py F.
可以看出有一条用例失败了,这个时候我们继续打开终端运行:pytest --lf
# tarzan @ tarzan3w in ~/Desktop [8:59:18]
$ pytest --lf
======================================================= test session starts ========================================================
platform linux -- Python 3.7.3, pytest-6.2.4, py-1.10.0, pluggy-0.13.1
rootdir: /home/tarzan/Desktop
plugins: allure-pytest-2.8.6, html-3.1.1, metadata-1.11.0, rerunfailures-10.1
collected 1 item
run-last-failure: rerun previous 1 failure
test_1.py F [100%]
============================================================= FAILURES =============================================================
___________________________________________________________ test_case_1 ____________________________________________________________
def test_case_1():
> assert 1 != 1
E assert 1 != 1
test_1.py:5: AssertionError
===================================================== short test summary info ======================================================
FAILED test_1.py::test_case_1 - assert 1 != 1
======================================================== 1 failed in 0.13s =========================================================
可以看出只运行了一条用例
断言
TODO
全局变量
pytest 在测试模块或 conftest.py
文件中定义时以特殊方式处理一些全局变量
可以在conftest.py 文件中声明以排除测试目录或模块
collect_ignore = ["setup.py"]
可以在conftest.py 文件中声明以排除带有 Unix shell 样式通配符的测试目录或模块
collect_ignore_glob = ["*_ignore.py"]
获取当前用例名称
var = os.environ.get('PYTEST_CURRENT_TEST')
可以在setup和teardown 还有当前用例中使用
foo_module.py::test_foo (setup)
foo_module.py::test_foo (call)
foo_module.py::test_foo (teardown)
本文是根据上海悠悠学习的过程中梳理的,防止以后忘了好查询