1. HTML测试报告
HTMLTestRunner 是 unittest 的一个扩展,它可以生成易于使用的 HTML 测试报告。可在Python3下运行的,地址:GitHub 地址:https://github.com/defnngj/HTMLTestRunner。
(1)下载与安装
打开上面的 GitHub 地址,克隆或下载整个项目。然后把 HTMLTestRunner.py 单独放到 Python 的安装目录下面,如 C:\Python37\Lib\。命令行窗口输入python,导入 HTMLTestRunner 验证安装是否成功。(如果提示找不到jinja2模块则通过“pip3 install jinja2”安装,若提示找不到__init__.py文件,则从下载的项目中找到和HTMLTestRunner.py文件放在一起的__init__.py文件也放到 Python 的安装目录下)
如果把 HTMLTestRunner 当作项目的一部分来使用,就把它放到项目目录中。(同时还需要把__init__.py 文件和 html文件夹放在项目目录中)例如:
unittest_expansion/
├─html/
│ └─template.html
│ └─…
├─test_case/
│ └─test_baidu.py
├─test_report/
├─__init__.py
├─HTMLTestRunner.py
└─run_tests.py
(2)生成 HTML 测试报告
查看第六篇文章中1.(4)中的 run_tests.py 文件,测试用例的执行是通过 TextTestRunner 类提供的 run()方法完成的。这里需要把 HTMLTestRunner.py 文件中的 HTMLTestRunner 类替换 TextTestRunner 类。
HTMLTestRunner 类 __init __() 初始化方法的参数如下:
- stream:指定生成 HTML 测试报告的文件,必填。
- verbosity:指定日志的级别,默认为 1。更详细的日志可以将参数修改为 2。
- title:指定测试用例的标题,默认为 None。
- description:指定测试用例的描述,默认为 None。
修改 run_tests.py 文件如下:
import unittest
from HTMLTestRunner import HTMLTestRunner
# 定义测试用例的目录为当前目录下的 test_cas 目录
test_dir = './test_case'
suit = unittest.defaultTestLoader.discover(test_dir, pattern='test*.py')
if __name__ == '__main__':
# 生成 HTML 格式的报告
fp = open('./test_report/result.html', 'wb')
runner = HTMLTestRunner(stream=fp,
title='百度搜索测试报告',
description='运行环境: Windows 10, Chrome 浏览器')
runner.run(suit)
fp.close()
使用 open()方法打开 result.html 文件,用于写入测试结果,没有时自动创建。最后,关闭 result.html 文件。
打开/test_report/result.html 文件:
(3)更易读的测试报告
现在生成的测试报告仅显示测试类名和测试方法名。在编写功能测试用例时,每条测试用例都有标题或说明,自动化测试用例也能加上中文的标题或说明。
Python 的注释有两种,一种叫作 comment,另一种叫作 doc string。前者为普通注释,后者用于描述函数、类和方法。
doc string 类型的注释在平时调用时不会显示,只有通过 help()方法查看时才会被显示出来。因为 HTMLTestRunner 可以读取 doc string 类型的注释,所以,我们只需给测试类或方法添加这种类型的注释即可。
class TestBaidu(unittest.TestCase):
""" 百度搜索测试 """
……
def test_search_key_selenium(self):
"""" 搜索关键字:selenium """
……
def test_search_key_unttest(self):
"""" 搜索关键字:unittest """
……
(4)测试报告文件名
因为测试报告的名称是固定的,所以每次新的测试报告都会覆盖上一次的。
我们最好能为测试报告自动取不同的名称,并且还要有一定的含义。时间是个不错的选择,因为它可以标识每个报告的运行时间,更主要的是,时间永远不会重复。
>>> import time
>>> time.time()
1626682967.8470786
>>> time.ctime()
'Mon Jul 19 16:23:01 2021'
>>> time.localtime()
time.struct_time(tm_year=2021, tm_mon=7, tm_mday=19, tm_hour=16, tm_min=23, tm_sec=10, tm_wday=0, tm_yday=200, tm_isdst=0)
>>> time.strftime("%Y_%m_%d %H:%M:%S")
'2021_07_19 16:24:23'
>>>
- time.time():获取当前时间戳。
- time.ctime():当前时间的字符串形式。
- time.localtime():当前时间的 struct_time 形式。
- time.strftime():用来获取当前时间,可以将时间格式化为字符串。
修改 run_tests.py,修改部分如下:
if __name__ == '__main__':
# 取当前日期时间
now_time = time.strftime("%Y-%m-%d %H_%M_%S")
# 生成 HTML 格式的报告
fp = open('./test_report/' + now_time + 'result.html', 'wb')
通过 strftime()方法以指定的格式获取当前日期时间,并赋值给 now_time 变量。将 now_time 通过加号(+)拼接到生成的测试报告的文件名中。
2. 数据驱动应用
数据驱动是自动化测试的一个重要功能,抛开单元测试框架谈数据驱动的使用是没有意义的。
(1)数据驱动
数据的改变(更新)驱动自动化的执行,从而引起测试结果的改变。我们可以直接理解成参数化,输入数据的不同从而引起输出结果的变化。
在 unittest 中,可以使用读取数据文件来实现参数化。
创建 baidu_data.csv 文件,如下:
创建 test_baidu_data.py 文件,代码如下:
import csv
import codecs
import unittest
from time import sleep
from itertools import islice
from selenium import webdriver
class TestBaidu(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome()
cls.base_url = "https://www.baidu.com"
@classmethod
def tearDownClass(cls):
cls.driver.quit()
def baidu_search(self, search_key):
self.driver.get(self.base_url)
self.driver.find_element_by_id('kw').send_keys(search_key)
self.driver.find_element_by_id('su').click()
sleep(3)
def test_search(self):
with codecs.open("baidu_data.csv", "r", "utf_8_sig") as f:
data = csv.reader(f)
for line in islice(data, 1, None):
search_key = line[1]
self.baidu_search(search_key)
if __name__ == '__main__':
unittest.main(verbosity=2)
这里所有的测试数据被当做一条测试用例执行,这样划分不合理,3条数据对应3条测试用例更为合适,前面执行失败的测试用例不影响后面的。
修改 test_baidu_data.py 文件,修改如下:
import csv
import codecs
import unittest
from time import sleep
from itertools import islice
from selenium import webdriver
class TestBaidu(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome()
cls.base_url = "https://www.baidu.com"
cls.test_data = []
with codecs.open("baidu_data.csv", "r", "utf_8_sig") as f:
data = csv.reader(f)
for line in islice(data, 1, None):
cls.test_data.append(line)
@classmethod
def tearDownClass(cls):
cls.driver.quit()
def baidu_search(self, search_key):
self.driver.get(self.base_url)
self.driver.find_element_by_id('kw').send_keys(search_key)
self.driver.find_element_by_id('su').click()
sleep(3)
def test_search_selenium(self):
self.baidu_search(self.test_data[0][1])
def test_search_unittest(self):
self.baidu_search(self.test_data[1][1])
def test_search_parameterized(self):
self.baidu_search(self.test_data[2][1])
if __name__ == '__main__':
unittest.main(verbosity=2)
从结果中看,3 条数据被当作 3 条测试用例执行了。
读取数据文件也带来了两点问题:
- 增加了读取的成本。任何数据文件都需要将文件中的数据读取到程序中。
- 不方便维护。
在 CSV 数据文件中,并不能直观体现出每一条数据对应的测试用例。上面获取数据也存在问题,如果在 CSV 文件中间插入了一条数据,那么测试用例获取到的测试数据很可能就是错的。
如果测试过程中需要用很多数据呢?那么我们需要知道,UI 自动化测试是站在用户的角度模拟用户的操作,输入大量数据的功能很少,若真的需要输入大量数据,那么可能用户体验做得不好。
读取数据文件并非完全没必要,比如一些自动化测试的配置就可以放到数据文件中,如运行环境、运行的浏览器等,放到配置文件中会更方便管理。
(2)Parameterized
Parameterized 是 Python 的一个参数化库,同时支持 unittest、Nose 和 pytest 单元测试框架。
Parameterized 支持 pip 安装。 pip install parameterized
用参数化库来实现参数化,创建 test_baidu_parameterized.py 文件,代码如下:
import unittest
from time import sleep
from selenium import webdriver
from parameterized import parameterized
class TestBaidu(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome()
cls.base_url = "https://www.baidu.com"
def baidu_search(self, search_key):
self.driver.get(self.base_url)
self.driver.find_element_by_id('kw').send_keys(search_key)
self.driver.find_element_by_id('su').click()
sleep(2)
# 通过 Parameterized 实现参数化
@parameterized.expand([
('case1', 'selenium'),
('case2', 'unittest'),
('case3', 'parameterized'),
])
def test_search(self, name, search_key):
self.baidu_search(search_key)
self.assertEqual(self.driver.title, search_key + "_百度搜索")
@classmethod
def tearDownClass(cls):
cls.driver.quit()
if __name__ == '__main__':
unittest.main(verbosity=2)
代码中导入parameterized 类,然后通过 @parameterized.expand() 来装饰测试用例 test_search()。
在@parameterized. expand()中,每个元组都可以被认为是一条测试用例。元组中的数据为该条测试用例变化的值。在测试用例中,通过参数(这里是 name 和 search_key)来取每个元组中的数据。
unittest 的 main()方法设置 verbosity 参数为 2,输出更详细的执行日志。
测试结果根据@parameterized.expand()中元组的个数来统计测试用例数的,参数化会自动加上“0”、“1”和“2”来区分每条测试用例,在元组中定义的 name 也会作为每条测试用例名称的后缀出现。
(3)DDT
DDT(Data-Driven Tests)是针对 unittest 单元测试框架设计的扩展库。允许使用不同的测试数据来运行一个测试用例,并将其展示为多个测试用例。DDT 支持 pip 安装。 pip install ddt
创建 test_baidu_ddt.py 文件,代码如下:
import unittest
from time import sleep
from selenium import webdriver
from ddt import ddt, data, file_data, unpack
@ddt
class TestBaidu(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.driver = webdriver.Chrome()
cls.base_url = "http://www.baidu.com"
def baidu_search(self, search_key):
self.driver.get(self.base_url)
self.driver.find_element_by_id('kw').send_keys(search_key)
self.driver.find_element_by_id('su').click()
sleep(2)
# 参数化使用方式一
@data(['case1', 'selenium'], ['case2', 'ddt'], ['case3', 'python'])
@unpack # @unpack,['case1', 'selenium']被分解开,按照用例中的两个参数传递
def test_search1(self, case, search_key):
print("第一组测试用例:", case)
self.baidu_search(search_key)
self.assertEqual(self.driver.title, search_key + "_百度搜索")
# 参数化使用方式二
@data(('case1', 'selenium'), ('case2', 'ddt'), ('case3', 'python'))
@unpack
def test_search2(self, case, search_key):
print("第二组测试用例:", case)
self.baidu_search(search_key)
self.assertEqual(self.driver.title, search_key + "_百度搜索")
# 参数化使用方式三
@data({'search_key': 'selenium'}, {'search_key': 'ddt'}, {'search_key': 'python'})
@unpack
def test_search3(self, search_key):
print("第三组测试用例:", search_key)
self.baidu_search(search_key)
self.assertEqual(self.driver.title, search_key + "_百度搜索")
@classmethod
def tearDownClass(cls):
cls.driver.quit()
if __name__ == '__main__':
unittest.main(verbosity=2)
使用 DDT 时,测试类需要通过@ddt 装饰器进行装饰。DDT 提供了不同形式的参数化,上述代码又三组参数化:列表,元组,字典。需要注意的是,字典的 key 与测试方法的参数要保持一致。
DDT 同样支持数据文件的参数化。它封装了数据文件的读取,让我们更专注于数据文件中的内容,以及在测试用例中的使用,无须关心数据文件如何被读取。
这里举例DDT支持的 JSON,YAML 格式数据文件。读取 YAML 格式文件 需要安装 PyYaml 模块(pip install pyyaml)。
分别创建 ddt_data_file.json 文件 和 ddt_data_file.yaml 文件,如下:(yaml 文件中的加 “-” 缩进表示 list 格式)
修改 test_baidu_ddt.py 文件,添加代码如下:
# 参数化读取 JSON 文件
@file_data('../data_file/ddt_data_file.json')
def test_search4(self, search_key):
print("第四组测试用例:", search_key)
self.baidu_search(search_key)
self.assertEqual(self.driver.title, search_key + "_百度搜索")
# 参数化读取 yaml 文件
@file_data('../data_file/ddt_data_file.yaml')
def test_search5(self, case):
search_key = case[0]['search_key']
print("第五组测试用例:", search_key)
self.baidu_search(search_key)
self.assertEqual(self.driver.title, search_key + "_百度搜索")
@classmethod
def tearDownClass(cls):
cls.driver.quit()
3. 自动发送邮件功能
自动发送邮件功能是自动化测试项目的重要需求之一,当自动化测试用例运行完成之后,可自动向相关人员的邮箱发送测试报告。发送邮件模块并不属于 unittest 的扩展,可以将它与 unittest 结合使用。
SMTP(Simple Mail Transfer Protocol)是简单邮件传输协议,是一组由源地址到目的地址传送邮件的规则,可以控制信件的中转方式。Python 的 smtplib 模块提供了简单的 API用来实现发送邮件功能,它对 SMTP 进行了简单的封装。
发邮件基础知识:有一个自己的邮箱,通过浏览器打开邮箱网址,或打开邮箱客户端登录自己的邮箱账号,若是邮箱客户端,则需要配置邮箱服务器地址。然后填写收件人地址、邮件的主题和正文,以及添加附件等。
(1)Python 自带的发送邮件功能
在发送邮件时,除填写主题和正文外,还可以增加抄送人、添加附件等,这里邮件正文和附近都是测试报告。
a. 发送邮件正文
import smtplib
from email.mime.text import MIMEText
from email.header import Header
# 发送邮件主题
subject = 'Python email test'
# 编写 HTML 类型的邮件正文
msg = MIMEText('<html><h1>你好!</h1></html>', 'html', 'utf-8')
msg['Subject'] = Header(subject, 'utf-8')
# 发送邮件
smtp = smtplib.SMTP()
smtp.connect('smtp.qq.com')
smtp.login('sender@qq.com', '*********') # 第二个参数为开启 POP3/SMTP 服务的授权码
smtp.sendmail('sender@qq.com', 'receiver@qq.com', msg.as_string())
smtp.quit()
登录收件人邮箱,查看邮件内容。
MIMEText 类,定义发送邮件的正文、格式,以及编码。
Header 类,定义邮件的主题和编码类型。
smtplib 模块用于发送邮件。connect()方法指定连接的邮箱服务;login()方法指定登录邮箱的账号和密码(此处是授权码);sendmail()方法指定发件人、收件人,以及邮件的正文; quit()方法用 于关闭邮件服务器的连接。
b. 发送带附件的邮件
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
# 邮件主题
subject = 'Python send email test'
# 发送的附件
with open('../data_file/attachment.txt', 'rb') as f:
send_att = f.read()
att = MIMEText(send_att, 'text', 'utf-8')
att['Content-Type'] = 'application/octet-stream' # 指定附件内容类型:二进制流
att['Content-Disposition'] = 'attachment; filename="attachment.txt"' # 指定显示附件的文件
msg = MIMEMultipart()
msg['Subject'] = subject
msg.attach(att)
# 发送邮件
smtp = smtplib.SMTP()
smtp.connect("smtp.qq.com")
smtp.login('sender@qq.com', '*********') # 第二个参数为开启 POP3/SMTP 服务的授权码
smtp.sendmail('sender@qq.com', 'receiver@qq.com', msg.as_string())
smtp.quit()
MIMEText 类,定义发送邮件的正文、格式,以及编码;Content-Type 指定附件内容类型;application/octet-stream 表示二进制流;Content-Disposition指定显示附件的文件;attachment; filename="attachment.txt"指定附件的文件名。
使用 MIMEMultipart 类定义邮件的主题,attach()指定附件信息。
(2)用 yagmail 发送邮件
yagmail 是 Python 的一个第三方库,可以让我们以非常简单的方法实现自动发送邮件功能。通过 pip 安装:pip install yagmail。yagmail 库极大地简化了发送邮件的代码。
import yagmail
# 连接邮箱服务器
yag = yagmail.SMTP(user='sender@qq.com', password='**********',
host='smtp.qq.com')
# 邮件正文
contents = ['This is the text, attached you can see a picture and a TXT file']
# 发送邮件
yag.send('receiver@qq.com', 'subject', contents)
代码中 password 为授权码。
若要给多个用户发送邮件,把收件人放到一个 list 中即可。
若要发送附件,给出文件路径参数即可,通过 list 可指定多个附件。
yag.send(['aa@qq.com', 'bb@qq.com', 'cc@qq.com'],
'subject', contents,
['../data_file/image.png', '../data_file/attachment.txt'])
(3)整合自动发送邮件
将发送邮件功能集成到自动化测试项目中,修改run_tests.py 文件,代码如下:
import time
import unittest
import yagmail
from HTMLTestRunner import HTMLTestRunner
# 把测试报告作为附件发送到指定邮箱
def send_email(report):
yag = yagmail.SMTP(user='1033756379@qq.com',
password='hspkyaqhbmpwbcea',
host='smtp.qq.com')
subject = "主题,自动化测试报告"
contents = "正文,请查看附件"
yag.send('2207087346@qq.com', subject, contents, report)
print("email has send out")
if __name__ == '__main__':
# 定义测试用例的目录为当前目录
test_dir = './test_case'
suit = unittest.defaultTestLoader.discover(test_dir, pattern='test*。py')
now_time = time.strftime("%Y-%m-%d %H_%M_%S")
html_report = './test_report' + now_time + 'result.html'
fp = open(html_report, 'wb')
# 调用 HTMLTestRunner,运行测试用例
runner = HTMLTestRunner(stream=fp,
title="百度搜索测试报告",
description="运行环境:Windows 10,Chrome 浏览器")
runner.run(suit)
fp.close()
send_email(html_report) # 发送报告
测试报告的内容不宜作为正文发送,因为 HTMLTestRunner 报告在展示时引用了 Bootstrap 样式库,当作为邮件正文“写死”在邮件中时,会导致样式丢失,所以作为附件发送更为合适。