原文:http://blog.csdn.net/huilan_same/article/details/76572411
感谢灰蓝大神!
本框架传送门:https://github.com/huilansame/Test_framework
在开始之前,请让我先声明几点:
- 请确保你已经掌握了基本的Python语法
- 如果你要搭建UI框架,请确保你已经掌握了Selenium的基本用法
- 这个框架主要面向刚刚会写脚本但是不知道该如何走向下一步的同学,欢迎吐槽,但最好带上改进建议
思考:我们需要一个什么样的框架
既然要搭一个框架,我们首先得弄明白我们需要一个什么样的框架,这个框架要支持什么功能?
框架主要的作用就是帮助我们编写更加简单而且好维护的用例,让我们把主要精力放在测试用例的设计上,那么我们就需要把所有额外的东西抽象出来作为框架的部分。
那么,额外的东西是什么?
- 日志以及报告
- 日志级别、URL、浏览器类型等基本配置
- 参数化
- 公共方法
搭建框架目录结构
现在我们很容易就把框架的结构搭建好了:
Test_framework
|--config(配置文件)
|--data(数据文件)
|--drivers(驱动)
|--log(日志)
|--report(报告)
|--test(测试用例)
|--utils(公共方法)
|--ReadMe.md(加个说明性的文件,告诉团队成员框架需要的环境以及用法)
接下来有一些选择题要做了:
Python 2 or 3? Selenium 2 or 3?
Python 3的使用越来越多,而且3的unittest中带有subTest,能够通过子用例实现参数化。而用2的话需要unittest2或其他的库来实现,所以我们这里选用python 3。
Selenium 3刚发布正式版不久,一些功能driver还没来得及跟上,尤其是geckodriver,所以选择Selenium 2(PY3一定要用selenium2.53.1)。
环境选择其实影响不大,你也可以选择你自己习惯的环境。
配置文件
配置文件我们有多种选择:ini、yaml、xml、properties、txt、py等
鉴于我之前写过一篇yaml的博文,我们这里就用yaml吧。
所以我们在config文件夹里创建config.yml文件,在utils里创建一个config.py文件读取配置,内容暂且不管。
简单的对之后的内容勾画一下
- 首先我们要把配置抽出来,用yaml文件放配置。所以我们要在config层添加配置文件config.yml,在utils层添加file_reader.py与config.py来管理。——怎样从0开始搭建一个测试框架_1
- 然后我们将python自带的logging模块封装了一下,从配置文件读取并设置固定的logger。在utils中创建了log.py。——怎样从0开始搭建一个测试框架_2
- 然后封装xlrd模块,读取excel,实现用例的参数化。——怎样从0开始搭建一个测试框架_3
- 然后是生成HTML测试报告,这个博主修改了网上原有的HTMLTestRunner,改为中文并美化,然后修改其支持PY3。你可以直接拿去用。——怎样从0开始搭建一个测试框架_4
- 然后我们给框架添加了发送邮件报告的能力。在utils中添加了mail.py。——怎样从0开始搭建一个测试框架_5
- 然后我们将测试用例用Page-Object思想进行封装,进一步划分test层的子层。——怎样从0开始搭建一个测试框架_6
- 接下来为了接口测试封装client类。在utils中添加了client.py。——怎样从0开始搭建一个测试框架_7
- 然后添加了一个简单的自定义断言,在utils中添加assertion.py,可用同样的方法自行扩展。——怎样从0开始搭建一个测试框架_8
- 接下来我们为了抽取响应结果,用JMESPath封装Extractor,在utils中添加extractor.py。——怎样从0开始搭建一个测试框架_9
- 然后是生成器。为我们自动生成固定类型的测试数据。utils下创建了generator.py。——怎样从0开始搭建一个测试框架_10
- 最后为了一些项目中的支持方法,如加密、签名等,创建支持库support.py。——怎样从0开始搭建一个测试框架_11
整个流程下来我们一个简单的框架就像模像样了,在此基础上可继续完善,实际用在项目中也没有什么问题,再简单结合 Jenkins
部署起来,定期或每次代码提交后可自动运行测试,直接把测试报告发送到项目成员手中,妥妥的!接下来就跟我一块学习吧。
这一步我们用到了selenium的基本的知识,以及一些unittest和PyYaml库的内容。
我们先创建一个简单的脚本吧,在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()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
脚本打开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()
- 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
- 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
现在,我们把配置抽出来放到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):
if not self._data:
with open(self.yamlf, 'rb') as f:
self._data = list(yaml.safe_load_all(f))
return self._data
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
而且我们需要一个Config类来读取配置,config.py:
"""
读取配置。这里配置文件用的yaml,也可用其他如XML,INI等,需在file_reader中添加相应的Reader进行处理。
"""
import os
from utils.file_reader import YamlReader
BASE_PATH = os.path.abspath(os.path.dirname(os.path.abspath(__file__)) + '\..')
CONFIG_FILE = BASE_PATH + '\config\config.yml'
DATA_PATH = BASE_PATH + '\data\\'
DRIVER_PATH = BASE_PATH + '\drivers\\'
LOG_PATH = BASE_PATH + '\log\\'
REPORT_PATH = 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)
- 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
- 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
修改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()
- 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
- 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
我们已经把配置分离出来了,虽然现在看起来似乎很麻烦,但是想想如果你有50个用例文件甚至更多,一旦项目URL变了,你还要一个个去修改吗?
这部分需要预先了解Python的内置库logging
接下来我们为我们的框架加上log,在utils中创建一个log.py文件,python有很方便的logging库,我们对其进行简单的封装,使框架可以很简单地打印日志(输出到控制台以及日志文件)。
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)
file_handler = TimedRotatingFileHandler(filename=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()
- 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
- 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
然后修改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()
- 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
- 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
执行后,可以看到在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 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)
file_handler = TimedRotatingFileHandler(filename=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()
- 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
- 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
现在,我们已经可以很方便地输出日志了,并且可以通过配置config.yml来修改log的设置。
所有的代码我都放到了GITHUB上传送,可以自己下载去学习,有什么好的建议或者问题,可以留言或者加我的QQ群:455478219讨论。
这一步我们需要用到Python库xlrd
我们已经把配置分离,并添加了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):
if not self._data:
with open(self.yamlf, 'rb') as f:
self._data = list(yaml.safe_load_all(f))
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)
for col in range(1, s.nrows):
self._data.append(dict(zip(title, s.row_values(col))))
else:
for col in range(0, s.nrows):
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)
- 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
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 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
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
我们添加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)
- 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
- 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
subTest是PY3 unittest里带的功能,PY2中没有,PY2中要想使用,需要用unittest2库。subTest是没有setUp和tearDown的,所以需要自己手动添加并执行。
现在我们就实现了数据分离,之后如果要搜索“张三”、“李四”,只要在excel里添加行就可以了。subTest参数化也帮助我们少写了很多用例方法,不用一遍遍在Case里copy and paste了。
这一步我们需要用到并修改HTMLTestRunner.py,它本身是基于PY2的,简单而实用,之前博主对其进行了美化,并且改成了中文(下载链接)。
现在博主基于此进行了对PY3的修改,增加了对subTest的支持。
- StringIO -> io
- 去掉decode
- 增加addSubTest()
部分修改内容:
import io
...
def startTest(self, test):
TestResult.startTest(self, test)
self.outputBuffer = io.StringIO()
...
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('\nTime Elapsed: %s' % (self.stopTime-self.startTime), file=sys.stderr)
return result
...
script = self.REPORT_TEST_OUTPUT_TMPL % dict(
id = tid,
output = saxutils.escape(o+e),
)
- 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
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 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
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
以上代码列出大部分主要修改。博主在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'))
- 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
- 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
执行后,可以在report目录下看到有 report.html
文件,我们已经生成测试报告了。
我们已经有了日志、有了报告,生成报告之后需要给其他组员看,自然要有发邮件的功能。这块我们要用到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))
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)
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))
- 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
- 76
- 77
- 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
- 76
- 77
之后我们修改用例文件,执行完成后发送邮件:
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()
- 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
- 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
执行完成之后可以看到receiver收到了我们的报告。当然,在这块你有可能遇到很多问题,可以根据错误号去网上查询如网易帮助。一般有几种常见的错误:
- 账户密码出错
- 服务器sever出错,这个可以根据你的发送人的邮箱去网站或邮箱设置中查看到
- 邮箱没有开通smtp服务,一般在邮箱设置中
- 邮件被拦截,在title、message以及发送的文件中不要带明显乱码、广告倾向的字符
- sender跟loginuser不一致的问题,发送人必须是登录用户
针对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()
- 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
- 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
这里做了非常简单的封装,可以根据传入的参数选择浏览器的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)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
我们仅仅封装了几个方法,更多的封装还请读者自己动手,接下来我们需要对页面进行封装,在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()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
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):
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)
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()
- 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
- 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
现在,我们已经用PO把用例改写了,这里面还有不少问题,浏览器的设置、基础page的封装、log太少、没有做异常处理等等,这些相信你都可以逐步完善的。
前面我们都是用的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
- 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
- 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
接下来写个用例,但是我们接口的用例跟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'))
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
这里我们加了一句断言,没有断言怎么能叫用例,我们之前写的UI用例,也可以自己动手加上断言。
现在我们的框架既可以做UI测试,也能做接口测试了。如果你的接口类型不是HTTP的,请自己封装对应的Client类。socket库测TCP接口、suds库测SOAP接口,不论你是什么类型的接口,总能找到对应的Python库的。
上次我们的用例中增加了断言。断言(检查点)这个东西对测试来说很重要。不然你怎么知道一个测试结果是对是错呢。unittest为我们提供了很多很好的断言,但是对于我们的项目可能是不够的。我们需要封装自己的断言方法。
这里我们简单封装一个断言,在utils中创建assertion.py文件,在其中创建断言:
"""
在这里添加各种自定义的断言,断言失败抛出AssertionError就OK。
"""
def assertHTTPCode(response, code_list=[200]):
res_code = response.status_code
if res_code not in code_list:
raise AssertionError('响应code不在列表中!')
这个断言传入响应,以及期望的响应码列表,如果响应码不在列表中,则断言失败。
在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'))
- 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
- 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
我们添加断言,响应码在[400]中,执行会发现fail掉了。
在assertion.py中你可以添加更多更丰富的断言,响应断言、日志断言、数据库断言等等,请自行封装。
对接口测试来说,很多时候,我们的用例不是一次请求就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)
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)
- 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
- 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
这样我们就完成了对JSON格式的抽取器,如果返回结果是JSON串,我们可以通过这个抽取器找到我们想要的数据,再进行下一步的操作,或者用来做断言。
这里仅仅完成了对JSON格式响应的抽取,之后读者可以自己添加XML格式、普通字符串格式、Header的抽取器,逐步进行完善。
有时候接口或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))
- 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
- 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
你还可以添加各种各样的生成器,比如指定长度中文、英文、特殊字符的字符串,指定格式的json串等等,可以省去很多构造测试数据的烦恼。
框架到这里已经很不错了,后面就需要各位自己去完善了。比如有时候请求需要加密、签名,还有一些支持方法,可以在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'))
- 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
- 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
根据你实际情况的不同,在其中添加其他支持方法。
就写这么多了,你可以根据这个思路补充扩充,来实现你自己的测试框架,也可以自己调整框架的分层与结构,框架的目的是为了简化我们用例编写和维护的工作量,也没必要把框架搞的太过复杂。
所有的代码我都放到了GITHUB上传送,可以自己下载去学习,有什么好的建议或者问题,可以留言或者加我的QQ群:455478219讨论。