参考文章:https://mp.weixin.qq.com/s/k6ZZ42CFZhq20RGuBcOOfQ
unittest是python自带的单元测试框架,它封装好了一些校验返回的结果方法和一些用例执行前的初始化操作,使得单元测试易于开展,因为它的易用性,很多同学也拿它来做功能测试和接口测试,只需简单开发一些功能(报告,初始化webdriver,或者http请求方法)便可实现。
但自动化测试中我们常常需要根据不同需求挑选部分测试用例运行,并且我们希望用例克服环境不稳定的局限,即运行失败后自动重新运行一次,如果成功就认为是环境问题导致第一次失败,还有我们经常希望测试用例可以并发执行等等,这些unittest都做不到或者需要大量二次开发才能做到,那么有没有更加强大的框架可以替代unittests呢?
pytest是python里的一个强大框架,它可以用来做单元测试,你也可以用来做功能,接口自动化测试。而且它比unittest支持的功能更多更全面。但是pytest在Getstarted里给出的实例却很简单,很多同学错以为它只是跟unittest一样是个单元测试框架罢了,如果你查询中文互联网,你也只能找到寥寥数篇大致一样的用法,可以说pytest的精髓使用,没有被大家挖掘出来,如此强大的框架不应该被埋没,今天我就带领大家深入pytest使用,共同领略pytest的强大。
1.pytst安装
pytest不属于python的标准库,所以需要安装才能使用, 安装方式如下:
1 |
pip install -U pytest |
如果你已经安装有pytest,想查看它的版本号:
1 |
pytest --version |
2.你的第一个pytest测试
1 2 3 4 5 6 7 8 |
#直接使用官网用例 # content of TesterTalk.py def func(x): return x + 1 def test_answer(): assert func(3) == 5 #在你的terminial里输入 python -m pytest TesterTalk.py,你将会看到如下信息: |
非常简单吧, 如果想运行多个用例该如何做呢?
1 2 3 4 5 6 7 8 9 10 11 |
# content of TesterTalk.py class TestClass(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #把要测试的case以test_开头就好了。 #terminal里输入 pytest TesterTalk.py, 执行结果一个成功一个失败。 |
注意:
(1).如果你想用pytest寻找整个文件夹下的测试用例,那么文件须以test_开头或者以test结尾。
(2).测试类以Test开头,并且不能带有 init 方法。
(3).测试函数以test开头。
(4).另外,pytest不支持也不打算支持中文路径,如果项目路径中有中文会报错。
好了,pytest的getStarted就结束了, 看了上面的应用方式的确没觉得它哪里强大。 别着急,我们再来想一想,如果你有个测试框架,你希望如何用这个框架做测试?
3.灵活的指定测试用例运行集。
在unittest框架里,你只能通过suite.addTest(),或者defaultTestLoader.Discover()两种方法在查找测试用例,对于你不需要的测试用例,只能用@unittest.skip()
来忽略,但做不到不改动代码变更测试用例集,pytest很好的实现了这一点,它支持如下查找:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# 1. 运行某个package下的所有用例。 pytest test_4. #会运行test_4下面所有的test_*.py 或者*_test.py文件里所有的Test开头的类(这个类不允许有__init__方法)下面的以test_开头的方法。 #2. 运行某个文件(module)下的所有用例。 pytest testtalk.py #会查找这个文件下所有的Test开头的类(这个类不允许有__init__方法)下面的以test_开头的方法。 #3. 运行包含(或不包含)某个关键词的测试类/方法。 class TestClass(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #在terminal里输入 pytst -k "TestClass and not two" #会运行 test_one这个function但不会与性test_two. #4.根据node ids运行测试用例。 pytest给每个test assign里一个id。 可以用::隔开。 举例来说: # TesterTalk.py class TestClass(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #在terminal里输入 pytest TesterTalk.py::TestClass 会执行TestClass下的所有用例 #在terminal里输入 pytest TesterTalk.py::TestClass::test_two, 仅会执行test_two. #5.根据标签(mark)运行,比如有个测试类,你想在regression时候才运行,另外一个测试类你想在releasae的时候才运行。 #TesterTalk.py @pytest.mark.release class TestClass1(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') @pytest.mark.regression class TestClass2(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #你只需在terminal里指定标签就可以了。 pytest TesterTalk.py -m release pytest TesterTalk.py -m regression 是不是比unittest方便太多了,所有的测试类,测试方法,你只需要用@pytest.mark.XXX装饰就好了。然后一次更改,多次挑选运行! 这样就完了吗? NO, No,No, 你如果想几个标签一起运行怎么办?或者你不想某个标签运行怎么办? Very simple: pytest TesterTalk.py -m “release or regression" #有release或者regression标签的都会运行。 pytest TesterTalk.py -m “not regression” # regression标签的不会运行。 当然了,unittest里支持的skip方法,pytest也支持,具体用法如下: #TesterTalk.py @pytest.mark.skip('skip') class TestClass1(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #skip装饰器可以放在类前面skip掉整个测试类,也可以防止function前面只skip掉这个function。 #对应的还有skipif, 请自己看文档了解。 |
难道就仅限于此吗? 其实pytest帮我们实现了更多的高级功能,比如:
4.并发运行测试用例集
关注公众号TesterTalk,跟我一起关注测试技术
首先,你得安装个插件:
1 |
pip install pytest-parallel #pytest-parallel比pytst-xdist要更好。 |
其次,要注意区这个插件仅仅支持python3.6版本及以上,而且如果你想多进程并发,必须跑在Unix或者Mac机器上,windows环境仅仅支持多线程运行。
运行上需要指定参数:
–workers (optional) X。 多进程运行, X是进程数。 默认值1。
–tests-per-worker (optional) X. 多线程运行, X是每个worker运行的最大并发线程数。 默认值1。
举例来说:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# TesterTalk.py import pytest @pytest.mark.release class TestClass1(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') @pytest.mark.regression class TestClass2(object): def test_one(self): x = "this" assert 'h' in x def test_two(self): x = "hello" assert hasattr(x, 'check') #terminal里输入 pytest TesterTalk.py --workers 2 #指定一个进程并发 #terminal里输入 pytest TesterTalk.py --workers 2 --test-per-worker 3 #指定2个进程并发,每个进程最多运行3个线程。 |
5.测试报告优化
1 2 3 4 5 6 7 8 |
#允许HTML格式报告 #首先还是要安装: pip install pytest-html Usage: 使用很简单带参数 --html=report.html就好。 仍然拿我们刚才举例: pytest TesterTalk.py --html=./report.html |
生成的结果如下:
有时候,我们需要克服环境问题,让失败的用例rerun,有没有办法呢?
1 2 3 4 5 6 7 8 9 |
#rerun失败的case #首先还是要安装: pip install pytest-rerunfailures Usage: --reruns 5 #rerun 5次 ----reruns 5 --reruns-delay 1 # rerun5次, 但是每次rerun前等待1秒。 仍以我们刚才的为例: #terminal里输入 pytest TesterTalk.py --reruns 1 --html=./report.html |
生成的结果如下:
可以看到,rerun聚合在了报告里。
我们自动化一般用到持续集成,Jenkins里需要junit XML格式的报告,pytest有没有办法直接生成?
1 2 3 4 5 6 |
无需安装,直接使用: pytest --junitxml=path 例如: pytest TesterTalk.py --reruns 1 --html=./report.html --junitxml=./xml_format_report.xml 会在本地目录生成xml_format_report.xml |
这就结束了吗?还远呢?数据参数化你了解下?
6.数据参数化
pytest有几种数据参数化方式:
pytest.fixture(). 不带参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#1.fixture()不带参数: import pytest @pytest.fixture() def initial_browser(): from selenium import webdriver return webdriver.Chrome() class TestBaidu(): def test_open_baidu(self, initial_browser): initial_browser.get("http://www.baidu.com") #在terminal里输入pytest testing/testertalk.py --html=./report.html, 你会看到百度浏览器打开了。说明函数间可以传递值,我们也可以利用这个来做unittest里setup()的事情。 |
pytest.fixture(), 带parms参数:
params with @pytest.fixture, a list of values for each of which the fixture function will execute and can access a value via request.param.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#利用fixtures的params
import pytest
@pytest.fixture(params=[{'username':'TesterTalk',"password":1}, {'username':'Test',"password":1}])
def account_provider(request): #request是固定的。
return request.param #request.parm也是固定的。
def test_login(account_provider):
#some operations
print(account_provider)
假设你需要用多个用户名密码测试登录,只需要用fixture()加params就好。
#在terminal里输入pytest testing/testertalk.py --html=./report.html
report显示下图,可以看到test_login被执行了2遍,每次执行带入的数据不同:
除了直接用pytest.fixture, 还可以这么用:
pytest.mark.usefixtures()
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import pytest @pytest.fixture(params=[{'username':'TesterTalk',"password":1}, {'username':'Test',"password":1}]) def account_provider(request): return request.param @pytest.mark.usefixtures("account_provider") def test_login(account_provider): #some operations print(account_provider) #在terminal里输入pytest testing/testertalk.py --html=./report.html,report跟上面的演示一样。 注意: 使用fixture标记函数后,函数将默认接入一个request参数,它将包含使用该fixture函数的信息,这使我们可以更加灵活的根据不同的函数来决定创建不同的对象以及释放函数。 举例来说userfixtures可以用作setup()和teardown() |
pytest固然强大,这就结束了吗?还有什么高阶的功能吗?必须的。
7.pytest.mark.parametrize实现数据驱动
1 2 3 4 5 6 7 8 9 10 11 |
#直接在测试方法前直到数据源 import pytest @pytest.mark.parametrize("account_provider", [{'username':'TesterTalk',"password":1}, {'username':'Test',"password":1}]) def test_login(account_provider): #some operations print(account_provider) #在terminal里输入pytest testing/testertalk.py --html=./report.html,就能看到login跑了2次。 |
如果我的数据来自外部文件呢?
1 2 3 4 5 6 7 8 9 10 |
import pytest #直接写函数读取外部文件生成数据值,注意values返回值是个list values = read_from_excel() @pytest.mark.parametrize('v', values) def test_login(v): #some operations print(v) #在terminal里输入pytest testing/testertalk.py --html=./report.html,就能看到login跑了2次,结果跟上面一样。 |
到这里为止,你已经学习了pytst的基础功能,高阶功能,还有什么吗? 如果你之前的框架是unittest, pytest支持无缝切换, 你不需要改任何代码。
记得上次直播我分享的unittest实现的自动化框架吗,我们看看这个page:
这个是unittest实现的测试类,我们之间在terminal里运行
1 |
pytet test_baidu.py --html=./report.html ,就是这么简单,我们看看报告是什么样子 |
怎么样,就问你惊喜不惊喜?!
当然,pytest的特色还远不只与此,我们最后介绍一个高级特性,它允许你在用例运行的整个session里,或者一个module里共享测试数据。
8.作用域(scope)实现数据共享(autouse)
我们知道,fixture,允许你不带参数运行和带参数运行, 调用fixture的第三种方式就是使用autouse
fixture decorator一个optional的参数是autouse, 默认设置为False。 当默认为False,就可以选择用上面两种方式来试用fixture。 当设置为True时,在一个session内的所有的test都会自动调用这个fixture。 权限大,责任也大,所以用该功能时也要谨慎小心。
举例来说,我想初始化我的浏览器,但是我不想每次测试运行都初始化,怎么办呢?我可以用scope限制住。
首先要建立一个conftest.py文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import pytest from selenium import webdriver @pytest.fixture(scope="session", autouse=True) def my_session_enginie_chrome(): print("I will use session engine -- chrome") browser= webdriver.Chrome() return browser @pytest.fixture(scope="module") def my_module_enginie_firefox(): print("I will use module engine -- firefox") browser = webdriver.Firefox() return browser @pytest.fixture(scope="module") def my_function_enginie_chrome(): print("I will use funciton engine --chrome") browser = webdriver.Firefox() return browser |
其次,写我们的测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#test_666.py import pytest ##如果autouse fixture被session装饰,那么它只会初始化一次,不管它在哪里定义的。通常用于全局系统初始化. @pytest.fixture(scope='session', autouse=True) def before(request): print ("session start") def test_browser(my_session_enginie_chrome, before): browser = my_session_enginie_chrome browser.get("https://wwww.baidu.com") assert 1==1 #整个module用,在这个module范围内可以用。 @pytest.fixture(scope='module', autouse=True) def before(request): print ("before start--%s" % request.module.__name__) def test_browser(my_module_enginie_firefox, before): browser = my_module_enginie_firefox browser.get("https://wwww.baidu.com") assert 1==1 # 每个test都会运行。 @pytest.fixture(scope='function', autouse=True) def before(request): print ("before start--%s" % request.function.__name__) def test_browser(my_function_enginie_chrome, before): browser = my_function_enginie_chrome browser.get("https://wwww.baidu.com") assert 1==1 |
fixture的存在使得我们在编写测试函数的准备函数、销毁函数或者多个条件的测试提供了更加灵活的选择。
autouse的scope含义如下:
autouse fixtures obey the scope= keyword-argument: if an autouse fixture has scope=’session’ it will only be run once, no matter where it is defined. scope=’class’ means it will be run once per class, etc.
if an autouse fixture is defined in a test module, all its test functions automatically use it.
if an autouse fixture is defined in a conftest.py file then all tests in all test modules below its directory will invoke the fixture.