前言

前几天,把之前同学写的接口测试脚本吃泡面哥py2转到py3,这过程很痛苦,因为不是自己写的代码,而且py2跟py3还是有写内置函数不一样的,恰好,之前的版本用到不少变化的函数,处理了半天,都不行,最终,自己重写了一遍,但整体架构还是跟之前同学设计的一样;

为什么不改?因为后面还是交给这个同学继续维护的; 那既然后续不是你维护,干嘛动别人的?因为要集成到jenkins,服务器不支持py2,也不想支持Py3;

当然有同学可能会,为啥不用官方的py2转py3脚本,这个得说明下,不是不用,而是用了之后,也有好多问题,各种环境需要配置,所以,还是重写来的快吧;

这里补充下,python的安装目录下是有一个叫2to3.py的脚本,这是官方提供把py2转py3的脚本,但并不是说用了这个脚本就不用管了,依然会有很多错误,需要人肉处理;

Windows下的路径如下:

C:\Users\jb\AppData\Local\Programs\Python\Python36\Tools\scripts
前面的是你Python的安装路径,相对路径就是Tools\scripts\2to3.py
复制代码



这个不是这次的重点,不花太多时间说明,感兴趣的可以自行了解下吧;



痛点

做任何事情,都是有痛点的,而这会的痛点在哪里?

想了很久很久,最痛的点就是回归测试太麻烦,其次是防止研发偷偷改接口;

为什么会这么说,是因为遇到类似的问题:
1)当业务多了,接口也跟随增加,如果每次全功能都回归所有的接口,那对于测试来说是极度的资源浪费,毕竟,大部分的工作都是重复性的,每个版本都需要做同样的事情;

2)对于后端同学来说,一般接口需要服务N个业务/产品,理论上来说,新功能要有新接口,但往往某些开发不自觉,直接在旧接口上处理,新增字段不止,还可以改以前的字段,这样,会导致其他依赖的产品会出现问题;

![image_1cnbn8gdo15821elk1o63nsk3es13.png-150.3kB][3]

3)测试操作比较繁琐,路径比较长,信息不公开,其他同学不知道等情况

鉴于上面的情况,如果有一个东西能帮忙自动回归验证的话,并且执行完毕后能输出报告,那就最好了;

于是乎,就有了接口自动化的乞丐版;

测试过程

先介绍下,常规接口测试的过程:
1、接口工具调用被测系统的接口(传参 username="jb")。
2、系统接口根据传参(username="jb")向正式数据库中查询数据。
3、将查询结果组装成一定格式的数据,并返回给被调用者。
4、人工或通过工具的断言功能检查接口测试的正确性。

那如果做成自动化,那过程是怎样:
1、接口测试项目先向测试数据库中插入测试数据(jb 的个人信息)。
2、调用被测系统接口(传参 username="jb")。
3、系统接口根据传参(username="jb")向测试数据库中进行查询并得到 jb 个人信息。
4、将查询结果组装成一定格式的数据,并返回给被调用者。
5、通过单元测试框架断言接口返回的数据(jb 的个人信息),并生成测试报告。

过程梳理

既然是接口自动化,那首先肯定要能发起请求->处理响应数据->生成测试报告->邮件发送

发起请求跟处理响应数据这块,直接使用Python requests库,原因是这个库处理起来比较方便,详情可以来这里了解;

测试报告的话,使用Python unittest;
邮件发送的话,使用Python email;

触发任务的话,可以使用APScheduler自动触发,也可以集成jenkins处理,本文采用jenkins处理,关于APScheduler,感兴趣的来这里看看;

ok,分工完毕了;

效果图

最终的效果就是这样,每天定时触发jenkins跑脚本,收到报告,仅此而已;



项目结构图:



这里说明下:
case存放的就是用例;
comment存放的是封装好的模块,比如读取、请求、报告样式、发送邮箱等;
data就是数据相关,比如截图判断,部分用例是存在xls里面,需要读取表格的参数作为请求的body信息;
report就是测试报告生成的地址;
config.ini就是数据库的配置信息
main.py就是主程序;

#unittest unittest是Python的单元测试框架,而大部分是用来组织用例以及测试执行;

##核心工作原理 unittest中最核心的四个概念是:
test case, test suite, test runner, test fixture



  • 一个TestCase的实例就是一个测试用例。 什么是测试用例呢?就是一个完整的测试流程,包括测试前准备环境的搭建(setUp),执行测试代码(run),以及测试后环境的还原(tearDown)。 单元测试(unit test)的本质也就在这里,一个测试用例是一个完整的测试单元,通过运行这个测试单元,可以对某一个问题进行验证。
  • 而多个测试用例集合在一起,就是TestSuite,而且TestSuite也可以嵌套TestSuite。
  • TestLoader是用来加载TestCase到TestSuite中的,其中有几个loadTestsFrom__()方法,就是从各个地方寻找TestCase,创建它们的实例,然后add到TestSuite中,再返回一个TestSuite实例。
  • TextTestRunner是来执行测试用例的,其中的run(test)会执行TestSuite/TestCase中的run(result)方法。 测试的结果会保存到TextTestResult实例中,包括运行了多少测试用例,成功了多少,失败了多少等信息。
  • 而对一个测试用例环境的搭建和销毁,是一个fixture。

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

到这里整个流程就清楚了:

  • 写好TestCase,然后由TestLoader加载TestCase到TestSuite
  • 然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,
  • main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。
  • 这里加个说明,在Runner执行时,默认将执行结果输出到控制台, 我们可以设置其输出到文件,在文件中查看结果

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):
    """Test mathfuc.py"""

    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方法是有问题的。

从这里有一些需要说明:

  • 在第一行给出了每一个用例执行的结果的标识,成功是 .,失败是 F,出错是 E,跳过是 S;
  • 每个测试方法均以 test开头,否则是不被unittest识别的;
  • verbosity 参数可以控制输出的错误报告的详细程度,默认是 1,如果设为 0,则不输出每一用例的执行结果,即没有上面的结果中的第1行;如果设为 2,则输出详细的执行结果

TestSuite

上面的展示多一个简单的测试用例,假如说,以后有很多用例,而且有写用例是由先后顺序的,怎么搞?

这时候就使用到TestSuite,添加到TestSuite中的case是会按照添加的顺序执行的。

这里感觉没什么好特别的,直接贴代码吧;

# 构建suite
    suite = unittest.TestSuite()
    # 通过discover把整个package的所有测试case都加入套件中

    all_cases = unittest.defaultTestLoader.discover(os.getcwd() +"/case/")
    for case in all_cases:
        suite.addTest(case)
复制代码

先构建一个suite,然后使用defaultTestLoader类下的discover方法,会自动根据测试目录匹配查找测试用例文件(test.py*),并将查找到的测试用例组装到测试套件,然后直接通过run()方法执行discover,如下:

discover=unittest.defaultTestLoader.discover(test_dir, pattern='test_*.py')
复制代码

不过本文没有这么做,只是指定了目录,然后遍历一遍,单独addTest;

把结果输出到文件中

用例准备好了,可以执行了,但执行的结果到哪里?不定义的话,是只在控制台输出,这肯定是不合理的,那如果想输出到文本的话,怎么搞?

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格式的测试报告了。

用HTMLTestRunner输出漂亮的HTML报告

如果只输出到TXT,那就很丑,有办法输出漂亮点的格式吗?



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

下载地址:

官方原版:tungwaiyip.info/software/HT…

然后修改下文件:

suite = unittest.TestSuite()
    suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestMathFunc))

    with open('HTMLReport.html', 'w') as f:
        runner = HTMLTestRunner(stream=f,
                                title='MathFunc Test Report',
                                description='generated by HTMLTestRunner.',
                                verbosity=2
                                )
        runner.run(suite)
复制代码

这样就能输出漂亮的格式啦,网上也有很多第三方的HTMLTestRunner文件,自己找下替换下即可;

截图使用的代码

# coding=utf-8
# import HTMLTestRunner
import os
import unittest
import commen.retry_HTMLTestRunner
import time
import commen.sendemail

# 设置报告文件保存路径
report_path = os.getcwd() + '/report/'

# 获取系统当前时间
now = time.strftime("%Y-%m-%d %H-%M", time.localtime(time.time()))

# 设置报告名称格式
html_file = report_path +"report"+ now + ".html"

file_name = "report"+ now + ".html"

if __name__ == '__main__':
    # 构建suite
    suite = unittest.TestSuite()
    # 通过discover把整个package的所有测试case都加入套件中

    all_cases = unittest.defaultTestLoader.discover(os.getcwd() +"/case/")
    for case in all_cases:
        suite.addTest(case)

    fp=open(html_file,'wb')

    # 初始化一个HTMLTestRunner实例对象,用来生成报告
    runner = commen.retry_HTMLTestRunner.HTMLTestRunner(stream=fp, title=u"互金跟投接口测试报告")
    # 开始执行套件
    runner.run(suite)
    commen.sendemail.sendemail(html_file,file_name)
复制代码

小小结

  • unittest是Python自带的单元测试框架,我们可以用其来作为我们自动化测试框架的用例组织执行框架。
  • unittest的流程:写好TestCase,然后由TestLoader加载TestCase到TestSuite,然后由TextTestRunner来运行TestSuite,运行的结果保存在TextTestResult中,我们通过命令行或者unittest.main()执行时,main会调用TextTestRunner中的run来执行,或者我们可以直接通过TextTestRunner来执行用例。
  • 一个class继承unittest.TestCase即是一个TestCase,其中以 test 开头的方法在load时被加载为一个真正的TestCase。
  • verbosity参数可以控制执行结果的输出,0 是简单报告、1 是一般报告、2 是详细报告。
  • 可以通过addTest和addTests向suite中添加case或suite,可以用TestLoader的loadTestsFrom__()方法。
  • 用 setUp()、tearDown()、setUpClass()以及 tearDownClass()可以在用例执行前布置环境,以及在用例执行后清理环境
  • 可以通过skip,skipIf,skipUnless装饰器跳过某个case,或者用TestCase.skipTest方法。
  • 参数中加stream,可以将报告输出到文件:可以用TextTestRunner输出txt报告,以及可以用HTMLTestRunner输出html报告。

这样,就能输出一份测试报告了;

断言方法

方法名

介绍

assertEqual(a,b,[msg='测试失败时打印的信息'])

断言a和b是否相等,相等则测试用例通过。

assertNotEqual(a,b,[msg='测试失败时打印的信息'])

断言a和b是否相等,不相等则测试用例通过。

assertTrue(x,[msg='测试失败时打印的信息'])

断言x是否True,是True则测试用例通过。

assertFalse(x,[msg='测试失败时打印的信息'])

断言x是否False,是False则测试用例通过。

assertIs(a,b,[msg='测试失败时打印的信息'])

断言a是否是b,是则测试用例通过。

assertNotIs(a,b,[msg='测试失败时打印的信息']

断言a是否是b,不是则测试用例通过。

assertIsNone(x,[msg='测试失败时打印的信息'])

断言x是否None,是None则测试用例通过。

assertIsNotNone(x,[msg='测试失败时打印的信息'])

断言x是否None,不是None则测试用例通过。

assertIn(a,b,[msg='测试失败时打印的信息'])

断言a是否在b中,在b中则测试用例通过。

assertNotIn(a,b,[msg='测试失败时打印的信息'])

断言a是否在b中,不在b中则测试用例通过。

assertIsInstance(a,b,[msg='测试失败时打印的信息'])

断言a是是b的一个实例,是则测试用例通过。

assertNotIsInstance(a,b,[msg='测试失败时打印的信息'])

断言a是是b的一个实例,不是则测试用例通过。

SMTP(发送邮件)

##基本信息介绍 SMTP(Simple Mail Transfer Protocol)即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。

python的smtplib提供了一种很方便的途径发送电子邮件。它对smtp协议进行了简单的封装。

Python创建 SMTP 对象语法如下:

import smtplib

smtpObj = smtplib.SMTP( [host [, port [, local_hostname]]] )
复制代码

参数说明:

  • host: SMTP 服务器主机。 你可以指定主机的ip地址或者域名如:runoob.com,这个是可选参数。
  • port: 如果你提供了 host 参数, 你需要指定 SMTP 服务使用的端口号,一般情况下 SMTP 端口号为25。
  • local_hostname: 如果 SMTP 在你的本机上,你只需要指定服务器地址为 localhost 即可。

Python SMTP 对象使用 sendmail 方法发送邮件,语法如下:

SMTP.sendmail(from_addr, to_addrs, msg[, mail_options, rcpt_options])

参数说明:

  • from_addr: 邮件发送者地址。
  • to_addrs: 字符串列表,邮件发送地址。
  • msg: 发送消息

这里要注意一下第三个参数,msg 是字符串,表示邮件。我们知道邮件一般由标题,发信人,收件人,邮件内容,附件等构成,发送邮件的时候,要注意 msg 的格式。这个格式就是 smtp 协议中定义的格式。

实例

这个例子包含了邮件里带附件的内容;

# coding:utf-8
import smtplib,time,re
from email.contentmanager import subtype
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

# 获取系统当前时间
now = time.strftime("%Y-%m-%d", time.localtime(time.time()))

def sendemail(html_file,name):
    host = 'smtp.51hjgt.com'  # 设置发件服务器地址
    port = 25  # 设置发件服务器端口号。注意,这里有SSL和非SSL两种形式
    sender = 'tester@51hjgt.com'  # 设置发件邮箱,一定要自己注册的邮箱
    pwd = 'Aa123456'  # 设置发件邮箱的密码,等会登陆会用到
    receiver = ['Talk.wei@100cb.cn','xiaoyi.huang@100cb.cn','guixiang.liu@100cb.cn']  # 设置邮件接收人,可以是扣扣邮箱
    #单个收件人的话,这样写 receiver = 'Talk.wei@100cb.cn','xiaoyi.huang@100cb.cn'

    with open(html_file, "r",encoding="utf-8") as f:
        body = f.read()   #设置邮件正文,这里是支持HTML的
        pattern = re.compile(r"<div id=\"chart\" style=\"width:50%[\s\S]*?</div>[\s\S]*?</div>")
        body = re.sub(pattern, "", body)


    msg = MIMEMultipart()
    msg['subject'] = '【DayBuild】' + now+"  互金跟投接口测试报告 " # 设置邮件标题
    msg['from'] = sender  # 设置发送人
    msg['to'] = ",".join(receiver)   # 设置接收人

    # 打开附件读取
    att = MIMEApplication(open(html_file, 'rb').read())
    att.add_header('Content-Disposition', 'attachment', filename=name)

    msg.attach(MIMEText(body, 'html'))  # 设置正文为符合邮件格式的HTML内容
    msg.attach(att)
    try:
        s = smtplib.SMTP(host, port)  # 注意!如果是使用SSL端口,这里就要改为SMTP_SSL
        s.login(sender, pwd)  # 登陆邮箱
        s.sendmail(sender, receiver, msg.as_string())  # 发送邮件!
        print('send email success')
    except smtplib.SMTPException:
        print('send email fail')
复制代码

就这样,测试报告跟邮件生成都有了,剩下就是自动构建任务执行脚本而已,这块使用jenkins就好了,这里不说明了,百度网上搜索吧;

readconfig

这个文件是用来读取配置信息

# -*- coding:utf-8 -*-
import configparser,pymysql,os
def readconfig(key,value):
    #读取配置文件的值
    cf = configparser.ConfigParser()
    cf.read(os.getcwd() +r'/config.ini')
    val=cf.get(key,value)
    return val

def dbconnectone(sql):

    #执行查询返回一条记录
    db = pymysql.connect(host=readconfig('db', 'host'), port=6033, user=readconfig('db', 'user'),
                         passwd=readconfig('db', 'pw'), db=readconfig('db', 'dbname'), charset='utf8')
    corsor = db.cursor()
    corsor.execute(sql)
    res = corsor.fetchone()
    return res
    db.close()
def dbconnectonec(sql):
    #执行查询返回一条记录
    db = pymysql.connect(host=readconfig('db', 'host'), port=6033, user=readconfig('db', 'user'),
                         passwd=readconfig('db', 'pw'), db=readconfig('db', 'dbname'), charset='utf8')
    corsor = db.cursor()
    corsor.execute(sql)
    res = corsor.fetchone()
    count = corsor.rowcount
    return res,count
    db.close()
def dbconnectall(sql):
    #执行查询,返回全部记录
    db = pymysql.connect(host=readconfig('db', 'host'), port=6033, user=readconfig('db', 'user'),
                         passwd=readconfig('db', 'pw'), db=readconfig('db', 'dbname'), charset='utf8')
    corsor = db.cursor()
    corsor.execute(sql)
    res = corsor.fetchall()
    count=corsor.rowcount
    return res,count
    db.close()

def dbupdate(sql):
    #修改数据库
    db = pymysql.connect(host=readconfig('db', 'host'), port=6033, user=readconfig('db', 'user'),
                         passwd=readconfig('db', 'pw'), db=readconfig('db', 'dbname'), charset='utf8')
    corsor = db.cursor()
    try:
        corsor.execute(sql)
        db.commit()
    except:
        db.rollback()
    db.close()
def dbupdatep(sql,param):
    #修改数据库
    db = pymysql.connect(host=readconfig('db', 'host'), port=6033, user=readconfig('db', 'user'),
                         passwd=readconfig('db', 'pw'), db=readconfig('db', 'dbname'), charset='utf8')
    corsor = db.cursor()
    try:
        corsor.execute(sql,param)
        db.commit()
    except:
        db.rollback()
    db.close()
def dbselectp(sql,param):
    #修改数据库
    db = pymysql.connect(host=readconfig('db', 'host'), port=6033, user=readconfig('db', 'user'),
                         passwd=readconfig('db', 'pw'), db=readconfig('db', 'dbname'), charset='utf8')
    corsor = db.cursor()
    corsor.execute(sql,param)
    res = corsor.fetchall()
    count = corsor.rowcount
    return res, count
    db.close()
复制代码

小结

本文主要介绍unittest跟SMTP的使用方法,其实把这两个了解怎么用,接口测试自动自动化就不难的,剩下的工作量无非就是看接口文档,根据要求请求参数及相关的body,然后用一些断言的方法来判断是否正确即可,这是最简单的接口自动化测试了,如果请求的时候需要body数据,这部分数据可以才用保存在xls文件里面进行读写,然后再组装;

谢谢大家~