搭建测试框架

Python单元测试框架unittest

关于python的测试框架,目前比较流行的有unittest、pytest、nosetest。
unittest是python内嵌的测试框架,原名为PyUnit。unittest提供了test cases、test suites、test fixtures、test runner相关的类,让测试更加明确、方便、可控。使用unittest编写用例,必须遵守以下规则:
1.测试文件必须先import unittest
2.测试类必须继承unittest.TestCase
3.测试方法必须以“test_”开头
4.测试类必须要有unittest.main()方法

pytest是python的第三方测试框架,是基于unittest的扩展框架,比unittest更简洁,更高效。使用pytest编写用例,必须遵守以下规则:
1.测试文件名必须以“test_”开头(如:test_ab.py)
2.测试方法必须以“test_”开头
pytest可以执行unittest风格的测试用例,无须修改unittest用例的任何代码,有较好的兼容性。
pytest插件丰富,比如flask插件,可用于用例出错重跑;还有xdist插件,可用于设备并行执行。

 

unittest中最核心概念是:test case, testsuite, TestLoder,TextTestRunner,TextTestResult, test fixture

TestCase(测试用例): 所有测试用例的基类,它是软件 测试中最基本的组成单元。 一个test case就是一个测试用例,是一个完整的测试流程,包括测试前环境的搭建setUp,执行测试代码(run),以及测试后环境的还原(tearDown)。测试用例是一个完整的测试单元,可以对某一问题进行验证。

TestSuite(测试套件):多个测试用例test case集合就是TestSuite,TestSuite可以嵌套TestSuite

TestLoder:是用来加载 TestCase到TestSuite中,其中有几个loadTestsFrom_()方法,就是从各个地方寻找TestCase,创建他们的实例,然后add到TestSuite中,再返回一个TestSuite实例

TextTestRunner:是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法。

 TextTestResult:测试结果会保存到TextTestResult实例中,包括运行了多少用例,成功与失败多少等信息

TestFixture:又叫测试脚手,测试代码的运行环境,指测试准备前和执行后要做的工作,包括setUp和tearDown方法

 

二、测试流程

1. 写好TestCase:一个class继承unittest.TestCase,就是一个测试测试用例,其中有多个以test开头的方法,那么 每一个这样的,在load的时候会生成一个TestCase实例。如果一个class中有四个test开头的方法,最后load到suite中时则有四个测试用例

2. 由TestLoder加载TestCase到TestSuite

3.然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中。

说明:a:通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者可以直接通过TextTestRunner来执行用例

b:Runner执行时,默认将结果输出到控制台,我们可以设置其输出到文件,在文件中查看 结果,也可以通过HTMLTestRunner将结果输出到HTML)

 

三、unittest实例

我们通过一些实例来更好地认识一下unittest。先来准备一些待测方法:mathfunc.py

def add(a, b):
    return a+b def minus(a, b): return a-b def multi(a, b): return a*b def divide(a, b): return a/b

接下来我们为这些方法写一个测试:test_mathfunc.py

# -*- coding: utf-8 -*-

import unittest
from mathfunc import *


class TestMathFunc(unittest.TestCase): #定义测试方法类,需继承unittest.TestCase """Test mathfuc.py""" def setUp(self):
print('每个用例执行前会调用setup方法准备环境')

def tearDown(self):
print('每个用例执行后会调用tearDown方法进行环境清理')
def test_add(self): """Test method add(a, b)""" self.assertEqual(3, add(1, 2)) self.assertNotEqual(3, add(2, 2)) def test_minus(self): """Test method minus(a, b)""" self.assertEqual(1, minus(3, 2)) def test_multi(self): """Test method multi(a, b)""" self.assertEqual(6, multi(2, 3)) def test_divide(self): """Test method divide(a, b)""" self.assertEqual(2, divide(6, 3)) self.assertEqual(2.5, divide(5, 2)) if __name__ == '__main__': unittest.main()

 

执行结果:

.F..
======================================================================
FAIL: test_divide (__main__.TestMathFunc)
Test method divide(a, b)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:/py/test_mathfunc.py", line 26, in test_divide self.assertEqual(2.5, divide(5, 2)) AssertionError: 2.5 != 2 ---------------------------------------------------------------------- Ran 4 tests in 0.000s FAILED (failures=1)

 

能够看到一共运行了4个测试,失败了1个,并且给出了失败原因,2.5 != 2 也就是说我们的divide方法是有问题的。这就是一个简单的测试,有几点需要说明的:

  1. 在第一行给出了每一个用例执行的结果的标识,成功是 .,失败是 F,出错是 E,跳过是 S。从上面也可以看出,测试的执行跟方法的顺序没有关系,test_divide写在了第4个,但是却是第2个执行的。

  2. 每个测试方法均以 test 开头,否则是不被unittest识别的。

  3. 在unittest.main()中加 verbosity 参数可以控制输出的错误报告的详细程度,默认是 1,如果设为 0,则不输出每一用例的执行结果,即没有上面的结果中的第1行;如果设为 2,则输出详细的执行结果

 

 

四、组织TestSuite

TestSuite可以解决如下几个问题:1、确定测试用例的顺序,哪个先执行哪个后执行。(我们添加到TestSuite中的case是会按照添加的顺序执行的)2、多个测试文件,怎么进行组织,而不是去一个个文件执行

 

TestLoder加载TestCase几种方法

 

在文件夹中我们再新建一个文件,test_suite.py:

# -*- coding: utf-8 -*-

import unittest
from test_mathfunc import TestMathFunc

if __name__ == '__main__': suite = unittest.TestSuite() tests = [TestMathFunc("test_add"), TestMathFunc("test_minus"), TestMathFunc("test_divide")] suite.addTests(tests) runner = unittest.TextTestRunner(verbosity=2) runner.run(suite)

五、将结果输出到文件中

用例组织好了,但结果只能输出到控制台,这样没有办法查看之前的执行记录,我们想将结果输出到文件。很简单,看示例:

修改test_suite.py:

# -*- coding: utf-8 -*-

import unittest
from test_mathfunc import TestMathFunc

if __name__ == '__main__': suite = unittest.TestSuite() suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc)) with open('UnittestTextReport.txt', 'a') as f: runner = unittest.TextTestRunner(stream=f, verbosity=2) runner.run(suite)

执行此文件,可以看到,在同目录下生成了UnittestTextReport.txt,所有的执行报告均输出到了此文件中,这下我们便有了txt格式的测试报告了。

 

六、test fixture之setUp() tearDown()

如果我的测试需要在每次执行之前准备环境,或者在每次执行完之后需要进行一些清理怎么办?比如执行前需要连接数据库,执行完成之后需要还原数据、断开连接。总不能每个测试方法中都添加准备环境、清理环境的代码吧。这就要涉及到我们之前说过的test fixture了。我们添加了 setUp() 和 tearDown() 两个方法(其实是重写了TestCase的这两个方法),这两个方法在每个测试方法执行前以及执行后执行一次,setUp用来为测试准备环境,tearDown用来清理环境,已备之后的测试。

如果想要在所有case执行之前准备一次环境,并在所有case执行结束之后再清理环境,我们可以用 setUpClass() 与 tearDownClass():

...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py""" @classmethod def setUpClass(cls): print "This setUpClass() method only called once." @classmethod def tearDownClass(cls): print "This tearDownClass() method only called once too." ...

执行结果如下:

...
This setUpClass() method only called once.
do something before test.Prepare environment.
add
do something after test.Clean up.
...
do something before test.Prepare environment.
multi
do something after test.Clean up.
This tearDownClass() method only called once too.

 

可以看到setUpClass以及tearDownClass均只执行了一次。

 

七、跳过某个case

如果我们临时想要跳过某个case不执行怎么办?unittest也提供了几种方法:

  1. skip装饰器
...

class TestMathFunc(unittest.TestCase):
    """Test mathfuc.py""" ... @unittest.skip("I don't want to run this case.") def test_divide(self): """Test method divide(a, b)""" print "divide" self.assertEqual(2, divide(6, 3)) self.assertEqual(2.5, divide(5, 2)) 

执行:

...
test_add (test_mathfunc.TestMathFunc)
Test method add(a, b) ... ok
test_divide (test_mathfunc.TestMathFunc)
Test method divide(a, b) ... skipped "I don't want to run this case."
test_minus (test_mathfunc.TestMathFunc)
Test method minus(a, b) ... ok test_multi (test_mathfunc.TestMathFunc) Test method multi(a, b) ... ok ---------------------------------------------------------------------- Ran 4 tests in 0.000s OK (skipped=1)

可以看到总的test数量还是4个,但divide()方法被skip了。skip装饰器一共有三个 unittest.skip(reason)unittest.skipIf(condition, reason)unittest.skipUnless(condition, reason),skip无条件跳过,skipIf当condition为True时跳过,skipUnless当condition为False时跳过。

 

 

八、用HTMLTestRunner输出HTML报告

HTMLTestRunner是一个第三方的unittest HTML报告库,首先我们下载HTMLTestRunner.py,并放到当前目录下

修改我们的test_suite.py

 

 

九、一些常用的断言方法

3.png

 

 

 

 

 

 

搭建框架目录结构

现在我们很容易就把框架的结构搭建好了:

Test_framework
    |--config(配置文件)
    |--data(数据文件)
    |--drivers(驱动)
    |--log(日志)
    |--report(报告)
    |--test(测试用例)
    |--utils(公共方法)
    |--ReadMe.md(加个说明性的文件,告诉团队成员框架需要的环境以及用法)

配置文件

配置文件我们有多种选择:ini、yaml、xml、properties、txt、py等

鉴于我之前写过一篇yaml的博文,我们这里就用yaml吧。

所以我们在config文件夹里创建config.yml文件,在utils里创建一个config.py文件读取配置,内容暂且不管。

简单的对之后的内容勾画一下

  1. 首先我们要把配置抽出来,用yaml文件放配置。所以我们要在config层添加配置文件config.yml,在utils层添加file_reader.py与config.py来管理。——怎样从0开始搭建一个测试框架_1
  2. 然后我们将python自带的logging模块封装了一下,从配置文件读取并设置固定的logger。在utils中创建了log.py。——怎样从0开始搭建一个测试框架_2
  3. 然后封装xlrd模块,读取excel,实现用例的参数化。——怎样从0开始搭建一个测试框架_3
  4. 然后是生成HTML测试报告,这个博主修改了网上原有的HTMLTestRunner,改为中文并美化,然后修改其支持PY3。你可以直接拿去用。——怎样从0开始搭建一个测试框架_4
  5. 然后我们给框架添加了发送邮件报告的能力。在utils中添加了mail.py。——怎样从0开始搭建一个测试框架_5
  6. 然后我们将测试用例用Page-Object思想进行封装,进一步划分test层的子层。——怎样从0开始搭建一个测试框架_6
  7. 接下来为了接口测试封装client类。在utils中添加了client.py。——怎样从0开始搭建一个测试框架_7
  8. 然后添加了一个简单的自定义断言,在utils中添加assertion.py,可用同样的方法自行扩展。——怎样从0开始搭建一个测试框架_8
  9. 接下来我们为了抽取响应结果,用JMESPath封装Extractor,在utils中添加extractor.py。——怎样从0开始搭建一个测试框架_9
  10. 然后是生成器。为我们自动生成固定类型的测试数据。utils下创建了generator.py。——怎样从0开始搭建一个测试框架_10
  11. 最后为了一些项目中的支持方法,如加密、签名等,创建支持库support.py。

 

怎样从0开始搭建一个测试框架_1——配置

我们先创建一个简单的脚本吧,在test文件夹创建test_baidu.py:

import os
import time
from selenium import webdriver
from selenium.webdriver.common.by import By URL = "http://www.baidu.com" base_path = os.path.dirname(os.path.abspath(__file__)) + '\..' driver_path = os.path.abspath(base_path+'\drivers\chromedriver.exe') locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') driver = webdriver.Chrome(executable_path=driver_path) driver.get(URL) driver.find_element(*locator_kw).send_keys('selenium 灰蓝') driver.find_element(*locator_su).click() time.sleep(2) links = driver.find_elements(*locator_result) for link in links: print(link.text) driver.quit()

 

脚本打开chrome,输入“selenium 灰蓝”,然后把所有结果中的标题打印出来。

如果想要搜索“Python selenium”,是不是要再创建一个脚本?还是把原来的脚本修改一下?

或者我们可以用unittest来改一下,把两次搜索分别写一个测试方法:

import os
import time
import unittest
from selenium import webdriver from selenium.webdriver.common.by import By class TestBaiDu(unittest.TestCase): URL = "http://www.baidu.com" base_path = os.path.dirname(os.path.abspath(__file__)) + '\..' driver_path = os.path.abspath(base_path+'\drivers\chromedriver.exe') locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def setUp(self): self.driver = webdriver.Chrome(executable_path=self.driver_path) self.driver.get(self.URL) def tearDown(self): self.driver.quit() def test_search_0(self): self.driver.find_element(*self.locator_kw).send_keys('selenium 灰蓝') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: print(link.text) def test_search_1(self): self.driver.find_element(*self.locator_kw).send_keys('Python selenium') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: print(link.text) if __name__ == '__main__': unittest.main()

现在,我们把配置抽出来放到config.yml中:

URL: http://www.baidu.com

为了读取yaml文件,我们需要一个封装YamlReader类,在utils中创建file_reader.py文件:

import yaml
import os


class YamlReader: def __init__(self, yamlf): if os.path.exists(yamlf): self.yamlf = yamlf else: raise FileNotFoundError('文件不存在!') self._data = None @property def data(self): # 如果是第一次调用data,读取yaml文档,否则直接返回之前保存的数据 if not self._data: with open(self.yamlf, 'rb') as f: self._data = list(yaml.safe_load_all(f)) # load后是个generator,用list组织成列表 return self._data

而且我们需要一个Config类来读取配置,config.py:

"""
读取配置。这里配置文件用的yaml,也可用其他如XML,INI等,需在file_reader中添加相应的Reader进行处理。
"""
import os
from utils.file_reader import YamlReader

# 通过当前文件的绝对路径,其父级目录一定是框架的base目录,然后确定各层的绝对路径。如果你的结构不同,可自行修改。 # 之前直接拼接的路径,修改了一下,用现在下面这种方法,可以支持linux和windows等不同的平台,也建议大家多用os.path.split()和os.path.join(),不要直接+'\\xxx\\ss'这样 BASE_PATH = os.path.split(os.path.dirname(os.path.abspath(__file__)))[0] CONFIG_FILE = os.path.join(BASE_PATH, 'config', 'config.yml') DATA_PATH = os.path.join(BASE_PATH, 'data') DRIVER_PATH = os.path.join(BASE_PATH, 'drivers') LOG_PATH = os.path.join(BASE_PATH, 'log') REPORT_PATH = os.path.join(BASE_PATH, 'report') class Config: def __init__(self, config=CONFIG_FILE): self.config = YamlReader(config).data def get(self, element, index=0): """ yaml是可以通过'---'分节的。用YamlReader读取返回的是一个list,第一项是默认的节,如果有多个节,可以传入index来获取。 这样我们其实可以把框架相关的配置放在默认节,其他的关于项目的配置放在其他节中。可以在框架中实现多个项目的测试。 """ return self.config[index].get(element)

修改test_baidu.py:

import time
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By from utils.config import Config, DRIVER_PATH class TestBaiDu(unittest.TestCase): URL = Config().get('URL') locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def setUp(self): self.driver = webdriver.Chrome(executable_path=DRIVER_PATH + '\chromedriver.exe') self.driver.get(self.URL) def tearDown(self): self.driver.quit() def test_search_0(self): self.driver.find_element(*self.locator_kw).send_keys('selenium 灰蓝') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: print(link.text) def test_search_1(self): self.driver.find_element(*self.locator_kw).send_keys('Python selenium') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: print(link.text) if __name__ == '__main__': unittest.main()

我们已经把配置分离出来了,虽然现在看起来似乎很麻烦,但是想想如果你有50个用例文件甚至更多,一旦项目URL变了,你还要一个个去修改吗?

 

 

怎样从0开始搭建一个测试框架_2——日志

接下来我们为我们的框架加上log,在utils中创建一个log.py文件,Python有很方便的logging库,我们对其进行简单的封装,使框架可以很简单地打印日志(输出到控制台以及日志文件)。

import os
import logging
from logging.handlers import TimedRotatingFileHandler
from utils.config import LOG_PATH class Logger(object): def __init__(self, logger_name='framework'): self.logger = logging.getLogger(logger_name) logging.root.setLevel(logging.NOTSET) self.log_file_name = 'test.log' self.backup_count = 5 # 日志输出级别 self.console_output_level = 'WARNING' self.file_output_level = 'DEBUG' # 日志输出格式 self.formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') def get_logger(self): """在logger中添加日志句柄并返回,如果logger已有句柄,则直接返回""" if not self.logger.handlers: # 避免重复日志 console_handler = logging.StreamHandler() console_handler.setFormatter(self.formatter) console_handler.setLevel(self.console_output_level) self.logger.addHandler(console_handler) # 每天重新创建一个日志文件,最多保留backup_count份 file_handler = TimedRotatingFileHandler(filename=os.path.join(LOG_PATH, self.log_file_name), when='D', interval=1, backupCount=self.backup_count, delay=True, encoding='utf-8' ) file_handler.setFormatter(self.formatter) file_handler.setLevel(self.file_output_level) self.logger.addHandler(file_handler) return self.logger logger = Logger().get_logger()

 

然后修改test_baidu.py,将输出改到log中:

import time
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By from utils.config import Config, DRIVER_PATH from utils.log import logger class TestBaiDu(unittest.TestCase): URL = Config().get('URL') locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def setUp(self): self.driver = webdriver.Chrome(executable_path=DRIVER_PATH + '\chromedriver.exe') self.driver.get(self.URL) def tearDown(self): self.driver.quit() def test_search_0(self): self.driver.find_element(*self.locator_kw).send_keys('selenium 灰蓝') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: logger.info(link.text) def test_search_1(self): self.driver.find_element(*self.locator_kw).send_keys('Python selenium') self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: logger.info(link.text) if __name__ == '__main__': unittest.main()

 

执行后,可以看到在log文件夹下创建了test.log文件,打印的信息都输出到了文件中:

2017-07-26 16:00:59,457 - framework - INFO - Python selenium —— 一定要会用selenium的等待,三种..._CSDN博客 2017-07-26 16:00:59,487 - framework - INFO - Selenium - 灰蓝 - CSDN博客 2017-07-26 16:00:59,515 - framework - INFO - ...教你在Windows上搭建Python+Selenium环境 - 灰蓝 - CSDN博客... 2017-07-26 16:00:59,546 - framework - INFO - Python selenium —— 父子、兄弟、相邻节点定位方式详..._CSDN博客 2017-07-26 16:00:59,572 - framework - INFO - Selenium - 灰蓝 - CSDN博客 2017-07-26 16:00:59,595 - framework - INFO - selenium之 时间日期控件的处理 - 灰蓝 - CSDN博客 ...

 

我们还可以把log的设置放到config中,修改config.yml,将几项重要的设置都写进去:

URL: http://www.baidu.com
log:
    file_name: test.log
    backup: 5 console_level: WARNING file_level: DEBUG pattern: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'

 

同时修改log.py,读取config,如果config中有,则采用文件中的设置,否则,采用默认设置

"""
日志类。通过读取配置文件,定义日志级别、日志文件名、日志格式等。
一般直接把logger import进去
from utils.log import logger
logger.info('test log')
"""
import os
import logging
from logging.handlers import TimedRotatingFileHandler from utils.config import LOG_PATH, Config class Logger(object): def __init__(self, logger_name='framework'): self.logger = logging.getLogger(logger_name) logging.root.setLevel(logging.NOTSET) c = Config().get('log') self.log_file_name = c.get('file_name') if c and c.get('file_name') else 'test.log' # 日志文件 self.backup_count = c.get('backup') if c and c.get('backup') else 5 # 保留的日志数量 # 日志输出级别 self.console_output_level = c.get('console_level') if c and c.get('console_level') else 'WARNING' self.file_output_level = c.get('file_level') if c and c.get('file_level') else 'DEBUG' # 日志输出格式 pattern = c.get('pattern') if c and c.get('pattern') else '%(asctime)s - %(name)s - %(levelname)s - %(message)s' self.formatter = logging.Formatter(pattern) def get_logger(self): """在logger中添加日志句柄并返回,如果logger已有句柄,则直接返回 我们这里添加两个句柄,一个输出日志到控制台,另一个输出到日志文件。 两个句柄的日志级别不同,在配置文件中可设置。 """ if not self.logger.handlers: # 避免重复日志 console_handler = logging.StreamHandler() console_handler.setFormatter(self.formatter) console_handler.setLevel(self.console_output_level) self.logger.addHandler(console_handler) # 每天重新创建一个日志文件,最多保留backup_count份 file_handler = TimedRotatingFileHandler(filename=os.path.join(LOG_PATH, self.log_file_name), when='D', interval=1, backupCount=self.backup_count, delay=True, encoding='utf-8' ) file_handler.setFormatter(self.formatter) file_handler.setLevel(self.file_output_level) self.logger.addHandler(file_handler) return self.logger logger = Logger().get_logger() 

 

现在,我们已经可以很方便地输出日志了,并且可以通过配置config.yml来修改log的设置。

 

 

怎样从0开始搭建一个测试框架_3——参数化

我们已经把配置分离,并添加了log,接下来我们应该尝试着进行数据分离,进行参数化了。

我们修改file_reader.py文件,添加ExcelReader类,实现读取excel内容的功能:

"""
文件读取。YamlReader读取yaml文件,ExcelReader读取excel。
"""
import yaml
import os
from xlrd import open_workbook class YamlReader: def __init__(self, yamlf): if os.path.exists(yamlf): self.yamlf = yamlf else: raise FileNotFoundError('文件不存在!') self._data = None @property def data(self): # 如果是第一次调用data,读取yaml文档,否则直接返回之前保存的数据 if not self._data: with open(self.yamlf, 'rb') as f: self._data = list(yaml.safe_load_all(f)) # load后是个generator,用list组织成列表 return self._data class SheetTypeError(Exception): pass class ExcelReader: """ 读取excel文件中的内容。返回list。 如: excel中内容为: | A | B | C | | A1 | B1 | C1 | | A2 | B2 | C2 | 如果 print(ExcelReader(excel, title_line=True).data),输出结果: [{A: A1, B: B1, C:C1}, {A:A2, B:B2, C:C2}] 如果 print(ExcelReader(excel, title_line=False).data),输出结果: [[A,B,C], [A1,B1,C1], [A2,B2,C2]] 可以指定sheet,通过index或者name: ExcelReader(excel, sheet=2) ExcelReader(excel, sheet='BaiDuTest') """ def __init__(self, excel, sheet=0, title_line=True): if os.path.exists(excel): self.excel = excel else: raise FileNotFoundError('文件不存在!') self.sheet = sheet self.title_line = title_line self._data = list() @property def data(self): if not self._data: workbook = open_workbook(self.excel) if type(self.sheet) not in [int, str]: raise SheetTypeError('Please pass in <type int> or <type str>, not {0}'.format(type(self.sheet))) elif type(self.sheet) == int: s = workbook.sheet_by_index(self.sheet) else: s = workbook.sheet_by_name(self.sheet) if self.title_line: title = s.row_values(0) # 首行为title for col in range(1, s.nrows): # 依次遍历其余行,与首行组成dict,拼到self._data中 self._data.append(dict(zip(title, s.row_values(col)))) else: for col in range(0, s.nrows): # 遍历所有行,拼到self._data中 self._data.append(s.row_values(col)) return self._data if __name__ == '__main__': y = 'E:\Test_framework\config\config.yml' reader = YamlReader(y) print(reader.data) e = 'E:/Test_framework/data/baidu.xlsx' reader = ExcelReader(e, title_line=True) print(reader.data) 

 

我们添加title_line参数,用来声明是否在excel表格里有标题行,如果有标题行,返回dict列表,否则返回list列表,如下:

# excel表格如下:
# | title1 | title2 |
# | value1 | value2 |
# | value3 | value4 |

# 如果title_line=True [{"title1": "value1", "title2": "value2"}, {"title1": "value3", "title2": "value4"}] # 如果title_line=False [["title1", "title2"], ["value1", "value2"], ["value3", "value4"]]

 

在data目录下创建baidu.xlsx,如下:

| search |
| selenium 灰蓝 |
| Python selenium |

 

然后我们再修改我们可怜的小用例:

import time
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By from utils.config import Config, DRIVER_PATH, DATA_PATH from utils.log import logger from utils.file_reader import ExcelReader class TestBaiDu(unittest.TestCase): URL = Config().get('URL') excel = DATA_PATH + '/baidu.xlsx' locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def sub_setUp(self): self.driver = webdriver.Chrome(executable_path=DRIVER_PATH + '\chromedriver.exe') self.driver.get(self.URL) def sub_tearDown(self): self.driver.quit() def test_search(self): datas = ExcelReader(self.excel).data for d in datas: with self.subTest(data=d): self.sub_setUp() self.driver.find_element(*self.locator_kw).send_keys(d['search']) self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: logger.info(link.text) self.sub_tearDown() if __name__ == '__main__': unittest.main(verbosity=2)

 

subTest是PY3 unittest里带的功能,PY2中没有,PY2中要想使用,需要用unittest2库。subTest是没有setUp和tearDown的,所以需要自己手动添加并执行。

现在我们就实现了数据分离,之后如果要搜索“张三”、“李四”,只要在excel里添加行就可以了。subTest参数化也帮助我们少写了很多用例方法,不用一遍遍在Case里copy and paste了。

 

 

怎样从0开始搭建一个测试框架_4——报告

这一步我们需要用到并修改HTMLTestRunner.py,它本身是基于PY2的,简单而实用,之前博主对其进行了美化,并且改成了中文(下载链接)。 
现在博主基于此进行了对PY3的修改,增加了对subTest的支持。 
【更新】增加了一个用Echarts做的统计图,见github

  1. StringIO -> io
  2. 去掉decode
  3. 增加addSubTest()

部分修改内容:

# import StringIO  # PY3改成了io库
import io
...
  def startTest(self, test): TestResult.startTest(self, test) # just one buffer for both stdout and stderr # self.outputBuffer = StringIO.StringIO() self.outputBuffer = io.StringIO() ... # 添加addSubTest方法,将有subTest的Case拆分成多个Case,均在报告中输出。这点处理与unittest的TextRunner并不相同,细心的同学可以试验下,看看它是怎么处理的。 def addSubTest(self, test, subtest, err): if err is not None: if getattr(self, 'failfast', False): self.stop() if issubclass(err[0], test.failureException): self.failure_count += 1 errors = self.failures errors.append((subtest, self._exc_info_to_string(err, subtest))) output = self.complete_output() self.result.append((1, test, output + '\nSubTestCase Failed:\n' + str(subtest), self._exc_info_to_string(err, subtest))) if self.verbosity > 1: sys.stderr.write('F ') sys.stderr.write(str(subtest)) sys.stderr.write('\n') else: sys.stderr.write('F') else: self.error_count += 1 errors = self.errors errors.append((subtest, self._exc_info_to_string(err, subtest))) output = self.complete_output() self.result.append( (2, test, output + '\nSubTestCase Error:\n' + str(subtest), self._exc_info_to_string(err, subtest))) if self.verbosity > 1: sys.stderr.write('E ') sys.stderr.write(str(subtest)) sys.stderr.write('\n') else: sys.stderr.write('E') self._mirrorOutput = True else: self.subtestlist.append(subtest) self.subtestlist.append(test) self.success_count += 1 output = self.complete_output() self.result.append((0, test, output + '\nSubTestCase Pass:\n' + str(subtest), '')) if self.verbosity > 1: sys.stderr.write('ok ') sys.stderr.write(str(subtest)) sys.stderr.write('\n') else: sys.stderr.write('.') ... def run(self, test): "Run the given test case or test suite." result = _TestResult(self.verbosity) test(result) self.stopTime = datetime.datetime.now() self.generateReport(test, result) # print >>>sys.stderr '\nTime Elapsed: %s' % (self.stopTime-self.startTime) # PY3的print需处理 print('\nTime Elapsed: %s' % (self.stopTime-self.startTime), file=sys.stderr) return result ... # PY3这里不用decode了,直接处理 # if isinstance(o,str): # # TODO: some problem with 'string_escape': it escape \n and mess up formating # # uo = unicode(o.encode('string_escape')) # # uo = o.decode('latin-1') # uo = o.decode('utf-8') # else: # uo = o # if isinstance(e,str): # # TODO: some problem with 'string_escape': it escape \n and mess up formating # # ue = unicode(e.encode('string_escape')) # # ue = e.decode('latin-1') # ue = e.decode('utf-8') # else: # ue = e script = self.REPORT_TEST_OUTPUT_TMPL % dict( id = tid, # output = saxutils.escape(uo+ue), output = saxutils.escape(o+e), )

 

以上代码列出大部分主要修改。博主在GitHub上上传了该文件,目前仅是简单做了点修改,没有经过正式的测试,之后可能会进行更多改动,感兴趣的可以star下来,或者自己进一步修改。

传送门

你也自己编写自己的Report Runner,并不很复杂。

将其放在utils目录中,然后我们再次修改test_baidu:

import time
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By from utils.config import Config, DRIVER_PATH, DATA_PATH, REPORT_PATH from utils.log import logger from utils.file_reader import ExcelReader from utils.HTMLTestRunner import HTMLTestRunner class TestBaiDu(unittest.TestCase): URL = Config().get('URL') excel = DATA_PATH + '/baidu.xlsx' locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def sub_setUp(self): self.driver = webdriver.Chrome(executable_path=DRIVER_PATH + '\chromedriver.exe') self.driver.get(self.URL) def sub_tearDown(self): self.driver.quit() def test_search(self): datas = ExcelReader(self.excel).data for d in datas: with self.subTest(data=d): self.sub_setUp() self.driver.find_element(*self.locator_kw).send_keys(d['search']) self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: logger.info(link.text) self.sub_tearDown() if __name__ == '__main__': report = REPORT_PATH + '\\report.html' with open(report, 'wb') as f: runner = HTMLTestRunner(f, verbosity=2, title='从0搭建测试框架 灰蓝', description='修改html报告') runner.run(TestBaiDu('test_search'))

 

执行后,可以在report目录下看到有 report.html 文件,我们已经生成测试报告了。

 

 

怎样从0开始搭建一个测试框架_5——邮件

我们已经有了日志、有了报告,生成报告之后需要给其他组员看,自然要有发邮件的功能。这块我们要用到smtplib和email库。

在utils中创建mail.py,初始化时传入全部所需数据,message是正文,可不填,path可以传list或者str;receiver支持多人,用”;”隔开就行

"""
邮件类。用来给指定用户发送邮件。可指定多个收件人,可带附件。
"""
import re
import smtplib
from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from socket import gaierror, error from utils.log import logger class Email: def __init__(self, server, sender, password, receiver, title, message=None, path=None): """初始化Email :param title: 邮件标题,必填。 :param message: 邮件正文,非必填。 :param path: 附件路径,可传入list(多附件)或str(单个附件),非必填。 :param server: smtp服务器,必填。 :param sender: 发件人,必填。 :param password: 发件人密码,必填。 :param receiver: 收件人,多收件人用“;”隔开,必填。 """ self.title = title self.message = message self.files = path self.msg = MIMEMultipart('related') self.server = server self.sender = sender self.receiver = receiver self.password = password def _attach_file(self, att_file): """将单个文件添加到附件列表中""" att = MIMEText(open('%s' % att_file, 'rb').read(), 'plain', 'utf-8') att["Content-Type"] = 'application/octet-stream' file_name = re.split(r'[\\|/]', att_file) att["Content-Disposition"] = 'attachment; filename="%s"' % file_name[-1] self.msg.attach(att) logger.info('attach file {}'.format(att_file)) def send(self): self.msg['Subject'] = self.title self.msg['From'] = self.sender self.msg['To'] = self.receiver # 邮件正文 if self.message: self.msg.attach(MIMEText(self.message)) # 添加附件,支持多个附件(传入list),或者单个附件(传入str) if self.files: if isinstance(self.files, list): for f in self.files: self._attach_file(f) elif isinstance(self.files, str): self._attach_file(self.files) # 连接服务器并发送 try: smtp_server = smtplib.SMTP(self.server) # 连接sever except (gaierror and error) as e: logger.exception('发送邮件失败,无法连接到SMTP服务器,检查网络以及SMTP服务器. %s', e) else: try: smtp_server.login(self.sender, self.password) # 登录 except smtplib.SMTPAuthenticationError as e: logger.exception('用户名密码验证失败!%s', e) else: smtp_server.sendmail(self.sender, self.receiver.split(';'), self.msg.as_string()) # 发送邮件 finally: smtp_server.quit() # 断开连接 logger.info('发送邮件"{0}"成功! 收件人:{1}。如果没有收到邮件,请检查垃圾箱,' '同时检查收件人地址是否正确'.format(self.title, self.receiver)) 

 

之后我们修改用例文件,执行完成后发送邮件:

import time
import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By from utils.config import Config, DRIVER_PATH, DATA_PATH, REPORT_PATH from utils.log import logger from utils.file_reader import ExcelReader from utils.HTMLTestRunner import HTMLTestRunner from utils.mail import Email class TestBaiDu(unittest.TestCase): URL = Config().get('URL') excel = DATA_PATH + '/baidu.xlsx' locator_kw = (By.ID, 'kw') locator_su = (By.ID, 'su') locator_result = (By.XPATH, '//div[contains(@class, "result")]/h3/a') def sub_setUp(self): self.driver = webdriver.Chrome(executable_path=DRIVER_PATH + '\chromedriver.exe') self.driver.get(self.URL) def sub_tearDown(self): self.driver.quit() def test_search(self): datas = ExcelReader(self.excel).data for d in datas: with self.subTest(data=d): self.sub_setUp() self.driver.find_element(*self.locator_kw).send_keys(d['search']) self.driver.find_element(*self.locator_su).click() time.sleep(2) links = self.driver.find_elements(*self.locator_result) for link in links: logger.info(link.text) self.sub_tearDown() if __name__ == '__main__': report = REPORT_PATH + '\\report.html' with open(report, 'wb') as f: runner = HTMLTestRunner(f, verbosity=2, title='从0搭建测试框架 灰蓝', description='修改html报告') runner.run(TestBaiDu('test_search')) e = Email(title='百度搜索测试报告', message='这是今天的测试报告,请查收!', receiver='...', server='...', sender='...', password='...', path=report ) e.send()

 

执行完成之后可以看到receiver收到了我们的报告。当然,在这块你有可能遇到很多问题,可以根据错误号去网上查询如网易帮助。一般有几种常见的错误:

  1. 账户密码出错
  2. 服务器sever出错,这个可以根据你的发送人的邮箱去网站或邮箱设置中查看到
  3. 邮箱没有开通smtp服务,一般在邮箱设置中
  4. 邮件被拦截,在title、message以及发送的文件中不要带明显乱码、广告倾向的字符
  5. sender跟loginuser不一致的问题,发送人必须是登录用户

 

 

怎样从0开始搭建一个测试框架_6——PageObject

针对UI自动化,接下来我们用PO思想进行下封装。

对于不同的项目,不同的页面,我们都需要选择浏览器、打开网址等,我们可以把这些操作抽象出来,让不同的用例去调用,只需要传入不同参数即可,不用一遍遍复制粘贴。

为此,我们对test目录再次进行分层,创建page、common、case、suite四个目录:

test
    |--case(用例文件)
    |--common(跟项目、页面无关的封装)
    |--page(页面)
    |--suite(测试套件,用来组织用例)

 

我们首先想要封装的选择浏览器、打开网址的类,所以放到common中,创建browser.py:

import time
import os
from selenium import webdriver
from utils.config import DRIVER_PATH, REPORT_PATH # 可根据需要自行扩展 CHROMEDRIVER_PATH = DRIVER_PATH + '\chromedriver.exe' IEDRIVER_PATH = DRIVER_PATH + '\IEDriverServer.exe' PHANTOMJSDRIVER_PATH = DRIVER_PATH + '\phantomjs.exe' TYPES = {'firefox': webdriver.Firefox, 'chrome': webdriver.Chrome, 'ie': webdriver.Ie, 'phantomjs': webdriver.PhantomJS} EXECUTABLE_PATH = {'firefox': 'wires', 'chrome': CHROMEDRIVER_PATH, 'ie': IEDRIVER_PATH, 'phantomjs': PHANTOMJSDRIVER_PATH} class UnSupportBrowserTypeError(Exception): pass class Browser(object): def __init__(self, browser_type='firefox'): self._type = browser_type.lower() if self._type in TYPES: self.browser = TYPES[self._type] else: raise UnSupportBrowserTypeError('仅支持%s!' % ', '.join(TYPES.keys())) self.driver = None def get(self, url, maximize_window=True, implicitly_wait=30): self.driver = self.browser(executable_path=EXECUTABLE_PATH[self._type]) self.driver.get(url) if maximize_window: self.driver.maximize_window() self.driver.implicitly_wait(implicitly_wait) return self def save_screen_shot(self, name='screen_shot'): day = time.strftime('%Y%m%d', time.localtime(time.time())) screenshot_path = REPORT_PATH + '\screenshot_%s' % day if not os.path.exists(screenshot_path): os.makedirs(screenshot_path) tm = time.strftime('%H%M%S', time.localtime(time.time())) screenshot = self.driver.save_screenshot(screenshot_path + '\\%s_%s.png' % (name, tm)) return screenshot def close(self): self.driver.close() def quit(self): self.driver.quit()

这里做了非常简单的封装,可以根据传入的参数选择浏览器的driver去打开对应的浏览器,并且加了一个保存截图的方法,可以保存png截图到report目录下。

我们再封装一个页面类Page:

from test.common.browser import Browser


class Page(Browser): # 更多的封装请自己动手... def __init__(self, page=None, browser_type='firefox'): if page: self.driver = page.driver else: super(Page, self).__init__(browser_type=browser_type) def get_driver(self): return self.driver def find_element(self, *args): return self.driver.find_element(*args) def find_elements(self, *args): return self.driver.find_elements(*args)

我们仅仅封装了几个方法,更多的封装还请读者自己动手,接下来我们需要对页面进行封装,在page目录创建如下两个文件:

baidu_main_page.py:

from selenium.webdriver.common.by import By
from test.common.page import Page


class BaiDuMainPage(Page): loc_search_input = (By.ID, 'kw') loc_search_button = (By.ID, 'su') def search(self, kw): """搜索功能""" self.find_element(*self.loc_search_input).send_keys(kw) self.find_element(*self.loc_search_button).click()

baidu_result_page.py:

from selenium.webdriver.common.by import By
from test.page.baidu_main_page import BaiDuMainPage


class BaiDuResultPage(BaiDuMainPage): loc_result_links = (By.XPATH, '//div[contains(@class, "result")]/h3/a') @property def result_links(self): return self.find_elements(*self.loc_result_links)

 

一个是封装的百度首页,一个封装百度结果页,这样,我们的测试用例就可以改为:

import time
import unittest
from utils.config import Config, DATA_PATH, REPORT_PATH
from utils.log import logger from utils.file_reader import ExcelReader from utils.HTMLTestRunner import HTMLTestRunner from utils.mail import Email from test.page.baidu_result_page import BaiDuMainPage, BaiDuResultPage class TestBaiDu(unittest.TestCase): URL = Config().get('URL') excel = DATA_PATH + '/baidu.xlsx' def sub_setUp(self): # 初始页面是main page,传入浏览器类型打开浏览器 self.page = BaiDuMainPage(browser_type='chrome').get(self.URL, maximize_window=False) def sub_tearDown(self): self.page.quit() def test_search(self): datas = ExcelReader(self.excel).data for d in datas: with self.subTest(data=d): self.sub_setUp() self.page.search(d['search']) time.sleep(2) self.page = BaiDuResultPage(self.page) # 页面跳转到result page links = self.page.result_links for link in links: logger.info(link.text) self.sub_tearDown() if __name__ == '__main__': report = REPORT_PATH + '\\report.html' with open(report, 'wb') as f: runner = HTMLTestRunner(f, verbosity=2, title='从0搭建测试框架 灰蓝', description='修改html报告') runner.run(TestBaiDu('test_search')) e = Email(title='百度搜索测试报告', message='这是今天的测试报告,请查收!', receiver='...', server='...', sender='...', password='...', path=report ) e.send()

 

现在,我们已经用PO把用例改写了,这里面还有不少问题,浏览器的设置、基础page的封装、log太少、没有做异常处理等等,这些相信你都可以逐步完善的。

 

 

怎样从0开始搭建一个测试框架_7——接口

前面我们都是用的UI自动化的用例来实现的,如果我们想做接口框架怎么办?今天就扩展一下接口测试模块,这里我们需要用到requests库(接口是HTTP类型的,其他类型也有对应的库)

我们先在ReadMe.md中补上新加的依赖库。然后在utils中创建一个client.py的文件,在其中创建一个HTTPClient类:

"""
添加用于接口测试的client,对于HTTP接口添加HTTPClient,发送http请求。
还可以封装TCPClient,用来进行tcp链接,测试socket接口等等。
"""

import requests
from utils.log import logger

METHODS = ['GET', 'POST', 'HEAD', 'TRACE', 'PUT', 'DELETE', 'OPTIONS', 'CONNECT'] class UnSupportMethodException(Exception): """当传入的method的参数不是支持的类型时抛出此异常。""" pass class HTTPClient(object): """ http请求的client。初始化时传入url、method等,可以添加headers和cookies,但没有auth、proxy。 >>> HTTPClient('http://www.baidu.com').send() <Response [200]> """ def __init__(self, url, method='GET', headers=None, cookies=None): """headers: 字典。 例:headers={'Content_Type':'text/html'},cookies也是字典。""" self.url = url self.session = requests.session() self.method = method.upper() if self.method not in METHODS: raise UnSupportMethodException('不支持的method:{0},请检查传入参数!'.format(self.method)) self.set_headers(headers) self.set_cookies(cookies) def set_headers(self, headers): if headers: self.session.headers.update(headers) def set_cookies(self, cookies): if cookies: self.session.cookies.update(cookies) def send(self, params=None, data=None, **kwargs): response = self.session.request(method=self.method, url=self.url, params=params, data=data, **kwargs) response.encoding = 'utf-8' logger.debug('{0} {1}'.format(self.method, self.url)) logger.debug('请求成功: {0}\n{1}'.format(response, response.text)) return response 

 

接下来写个用例,但是我们接口的用例跟UI混在一起总是不好,所以我们可以在test下创建一个interface的目录,里面创建test_baidu_http.py的用例文件。

这里你也可以在test下分成API和UI两层,分别在其中再进行分层,看情况而定吧。

test_baidu_http.py:

import unittest
from utils.config import Config, REPORT_PATH
from utils.client import HTTPClient from utils.log import logger from utils.HTMLTestRunner import HTMLTestRunner class TestBaiDuHTTP(unittest.TestCase): URL = Config().get('URL') def setUp(self): self.client = HTTPClient(url=self.URL, method='GET') def test_baidu_http(self): res = self.client.send() logger.debug(res.text) self.assertIn('百度一下,你就知道', res.text) if __name__ == '__main__': report = REPORT_PATH + '\\report.html' with open(report, 'wb') as f: runner = HTMLTestRunner(f, verbosity=2, title='从0搭建测试框架 灰蓝', description='接口html报告') runner.run(TestBaiDuHTTP('test_baidu_http'))

 

这里我们加了一句断言,没有断言怎么能叫用例,我们之前写的UI用例,也可以自己动手加上断言。

现在我们的框架既可以做UI测试,也能做接口测试了。如果你的接口类型不是HTTP的,请自己封装对应的Client类。socket库测TCP接口、suds库测SOAP接口,不论你是什么类型的接口,总能找到对应的Python库的。

 

 

 

怎样从0开始搭建一个测试框架_8——断言

上次我们的用例中增加了断言。断言(检查点)这个东西对测试来说很重要。不然你怎么知道一个测试结果是对是错呢。unittest为我们提供了很多很好的断言,但是对于我们的项目可能是不够的。我们需要封装自己的断言方法。

这里我们简单封装一个断言,在utils中创建assertion.py文件,在其中创建断言:

"""
在这里添加各种自定义的断言,断言失败抛出AssertionError就OK。
"""


def assertHTTPCode(response, code_list=None): res_code = response.status_code if not code_list: code_list = [200] if res_code not in code_list: raise AssertionError('响应code不在列表中!') # 抛出AssertionError,unittest会自动判别为用例Failure,不是Error 

 

这个断言传入响应,以及期望的响应码列表,如果响应码不在列表中,则断言失败。

在test_baidu_http.py中添加此断言:

import unittest
from utils.config import Config, REPORT_PATH
from utils.client import HTTPClient from utils.log import logger from utils.HTMLTestRunner import HTMLTestRunner from utils.assertion import assertHTTPCode class TestBaiDuHTTP(unittest.TestCase): URL = Config().get('URL') def setUp(self): self.client = HTTPClient(url=self.URL, method='GET') def test_baidu_http(self): res = self.client.send() logger.debug(res.text) assertHTTPCode(res, [400]) self.assertIn('百度一下,你就知道', res.text) if __name__ == '__main__': report = REPORT_PATH + '\\report.html' with open(report, 'wb') as f: runner = HTMLTestRunner(f, verbosity=2, title='从0搭建测试框架 灰蓝', description='接口html报告') runner.run(TestBaiDuHTTP('test_baidu_http'))

 

我们添加断言,响应码在[400]中,执行会发现fail掉了。

在assertion.py中你可以添加更多更丰富的断言,响应断言、日志断言、数据库断言等等,请自行封装。

 

 

 

怎样从0开始搭建一个测试框架_9——抽取器

对接口测试来说,很多时候,我们的用例不是一次请求就OK了的,而是多个请求复合的,我们第二个请求可能会用到第一个请求返回值中的数据,这就要我们再次进行封装,做一个抽取器,从结果中抽取部分信息。 
这里我们会用到JMESPath库,这是一个让我们通过类似于xpath或点分法来定位json中的节点的库

别忘了我们先在ReadMe.md中添加上依赖的库。

我们在utils中创建extractor.py文件,实现对响应中数据的抽取

"""抽取器,从响应结果中抽取部分数据"""

import json
import jmespath


class JMESPathExtractor(object): """ 用JMESPath实现的抽取器,对于json格式数据实现简单方式的抽取。 """ def extract(self, query=None, body=None): try: return jmespath.search(query, json.loads(body)) except Exception as e: raise ValueError("Invalid query: " + query + " : " + str(e)) if __name__ == '__main__': from utils.client import HTTPClient res = HTTPClient(url='http://wthrcdn.etouch.cn/weather_mini?citykey=101010100').send() print(res.text) # {"data": { # "yesterday": {"date": "17日星期四", "high": "高温 31℃", "fx": "东南风", "low": "低温 22℃", "fl": "<![CDATA[<3级]]>", # "type": "多云"}, # "city": "北京", # "aqi": "91", # "forecast": [ # {"date": "18日星期五", "high": "高温 28℃", "fengli": "<![CDATA[<3级]]>", "low": "低温 22℃", "fengxiang": "东北风", # "type": "多云"}, # {"date": "19日星期六", "high": "高温 29℃", "fengli": "<![CDATA[<3级]]>", "low": "低温 22℃", "fengxiang": "东风", # "type": "雷阵雨"}, # {"date": "20日星期天", "high": "高温 29℃", "fengli": "<![CDATA[<3级]]>", "low": "低温 23℃", "fengxiang": "东南风", # "type": "阴"}, # {"date": "21日星期一", "high": "高温 30℃", "fengli": "<![CDATA[<3级]]>", "low": "低温 24℃", "fengxiang": "西南风", # "type": "晴"}, # {"date": "22日星期二", "high": "高温 29℃", "fengli": "<![CDATA[<3级]]>", "low": "低温 24℃", "fengxiang": "北风", # "type": "雷阵雨"} # ], # "ganmao": "各项气象条件适宜,无明显降温过程,发生感冒机率较低。", "wendu": "25" # }, # "status": 1000, # "desc": "OK"} j = JMESPathExtractor() j_1 = j.extract(query='data.forecast[1].date', body=res.text) j_2 = j.extract(query='data.ganmao', body=res.text) print(j_1, j_2) # 结果: # 19日星期六 各项气象条件适宜,无明显降温过程,发生感冒机率较低。

 

这样我们就完成了对JSON格式的抽取器,如果返回结果是JSON串,我们可以通过这个抽取器找到我们想要的数据,再进行下一步的操作,或者用来做断言。

这里仅仅完成了对JSON格式响应的抽取,之后读者可以自己添加XML格式、普通字符串格式、Header的抽取器,逐步进行完善。

 

 

 

怎样从0开始搭建一个测试框架_10——数据生成器

有时候接口或UI上传入的数据需要符合指定的格式,我们在参数化的过程中又不愿意在excel中一遍遍去构造这样的数据,这时我们可以加入生成器来为我们产生符合某些固定格式的数据。 
这里我推荐一个挺有意思的库,Faker,能够为你产生各种假数据

别忘了在ReadMe.md中添上你要用的库。

在utils中创建一个generator.py,用来生成数据

"""一些生成器方法,生成随机数,手机号,以及连续数字等"""
import random
from faker import Factory

fake = Factory().create('zh_CN') def random_phone_number(): """随机手机号""" return fake.phone_number() def random_name(): """随机姓名""" return fake.name() def random_address(): """随机地址""" return fake.address() def random_email(): """随机email""" return fake.email() def random_ipv4(): """随机IPV4地址""" return fake.ipv4() def random_str(min_chars=0, max_chars=8): """长度在最大值与最小值之间的随机字符串""" return fake.pystr(min_chars=min_chars, max_chars=max_chars) def factory_generate_ids(starting_id=1, increment=1): """ 返回一个生成器函数,调用这个函数产生生成器,从starting_id开始,步长为increment。 """ def generate_started_ids(): val = starting_id local_increment = increment while True: yield val val += local_increment return generate_started_ids def factory_choice_generator(values): """ 返回一个生成器函数,调用这个函数产生生成器,从给定的list中随机取一项。 """ def choice_generator(): my_list = list(values) # rand = random.Random() while True: yield random.choice(my_list) return choice_generator if __name__ == '__main__': print(random_phone_number()) print(random_name()) print(random_address()) print(random_email()) print(random_ipv4()) print(random_str(min_chars=6, max_chars=8)) id_gen = factory_generate_ids(starting_id=0, increment=2)() for i in range(5): print(next(id_gen)) choices = ['John', 'Sam', 'Lily', 'Rose'] choice_gen = factory_choice_generator(choices)() for i in range(5): print(next(choice_gen)) 

 

你还可以添加各种各样的生成器,比如指定长度中文、英文、特殊字符的字符串,指定格式的json串等等,可以省去很多构造测试数据的烦恼。

 

 

 

怎样从0开始搭建一个测试框架_11——支持方法

框架到这里已经很不错了,后面就需要各位自己去完善了。比如有时候请求需要加密、签名,还有一些支持方法,可以在utils中建个support.py放进去。

在utils中创建一个support.py文件,里面可以放需要的一些支持方法,我们示例一个加密和签名的方法:

"""一些支持方法,比如加密"""
import hashlib
from utils.log import logger


class EncryptError(Exception): pass def sign(sign_dict, private_key=None, encrypt_way='MD5'): """传入待签名的字典,返回签名后字符串 1.字典排序 2.拼接,用&连接,最后拼接上私钥 3.MD5加密""" dict_keys = sign_dict.keys() dict_keys.sort() string = '' for key in dict_keys: if sign_dict[key] is None: pass else: string += '{0}={1}&'.format(key, sign_dict[key]) string = string[0:len(string) - 1] string = string.replace(' ', '') return encrypt(string, salt=private_key, encrypt_way=encrypt_way) def encrypt(string, salt='', encrypt_way='MD5'): u"""根据输入的string与加密盐,按照encrypt方式进行加密,并返回加密后的字符串""" string += salt if encrypt_way.upper() == 'MD5': hash_string = hashlib.md5() elif encrypt_way.upper() == 'SHA1': hash_string = hashlib.sha1() else: logger.exception(EncryptError('请输入正确的加密方式,目前仅支持 MD5 或 SHA1')) return False hash_string.update(string.encode()) return hash_string.hexdigest() if __name__ == '__main__': print(encrypt('100000307111111'))

根据你实际情况的不同,在其中添加其他支持方法。

 

转载于:https://www.cnblogs.com/tester-l/p/9566665.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值