接口测试(二)代码接口测试1:Python+Request+UnitTest+PyMySQL基础知识

目录

1.PyMySQL

1.1PyMySQL安装:

1.2数据库-基础操作流程

1.3工具类:DBUtils封装【重点】

2.Requests库

2.1 安装requests库

2.2 请求中常见的数据传递方式

2.3响应内容解析

2.4用Header 或Cookie 传递token

(1)Header 传递

(2)Cookie 传递

2.5session跨请求保持cookies

3.UnitTest

3.1unittest介绍

3.2使用UnitTest目的

3.3 UnitTest基本使用

(1)实现思路

(2)代码实现

(3)生成unittest测试报告

3.4 unittest参数化【重点】


前言:本文分模块介绍了PyMySQL数据库操作、Requests接口调用以及UnitTest测试框架,其中基于UnitTest参数化技术驱动data.json测试数据并生成可视化报告。但没有使用到PyMySQL,PyMySQL的实际应用放到接口测试框架搭建中讲解。

1.PyMySQL

PyMySQL是Python操作MySQL数据库的驱动。

1.1PyMySQL安装:

安装:pip install PyMySQL

校验:pip show pymsyql

1.2数据库-基础操作流程

1.创建连接

2.创建游标

3.执行sql

        3.1查询操作(select)

        3.2非查询操作(insert/update/delete)

                (1)事务提交(连接对象.commit())

                (2)事务回滚(连接对象.rollback())

4.关闭游标

5.关闭连接

2.1 查询类操作

import pymysql

# 创建连接
conn = pymysql.connect(host="localhost",
                       port=3306,
                       user="root",
                       password="Aa123456.",
                       database="books",
                       autocommit=False) # 默认是False。查询操作不需要开启事务;非查询操作需开始事务True自动提交事务
# 获取游标
cursor = conn.cursor()
# 执行sql
cursor.execute("select * from t_book")

print("游标初始位置:",cursor.rownumber) # 游标初始位置是0

print("获取查询结果的总记录数:",cursor.rowcount)
print("获取查询结果中第一条数据:",cursor.fetchone())
cursor.rownumber=0 # 注意这里需要重置游标
print("获取全部的查询结果:",cursor.fetchall())

print("游标结束位置:",cursor.rownumber) #游标结束位置是4


# 关闭游标
cursor.close()
# 关闭连接
conn.close()

2.2 非查询类操作

import pymysql

# 创建连接
conn = pymysql.connect(host="localhost",
                       port=3306,
                       user="root",
                       password="root",
                       database="books",
                       autocommit=True) # 非查询操作需开始事务True自动提交事务
# 获取游标
cursor = conn.cursor()
# 执行sql
sql="delete from t_book where title='自传';"
cursor.execute(sql)
print("获取查询结果的总记录数:",cursor.rowcount)
# 关闭游标
cursor.close()
# 关闭连接
conn.close()

1.3工具类:DBUtils封装【重点】

1.创建连接

2.创建游标

3.执行sql

        try:

                # 获取游标对象

                # 调用游标对象.execute(sql)

                # 如果是查询:

                        # 返回所有数据

                # 否则:

                        # 提交事务

                        # 返回受影响的行数

        except:

                # 回滚事务

                # 抛出异常

        finally:

                # 调用关闭游标

                # 调用关闭连接

4.关闭游标

5.关闭连接

DBUtils代码

# -*- coding: UTF-8 -*-

# 导包
import pymysql

# 创建工具类
class DBUtils():
    # 初始化
    __conn = None # __代表私有
    __cursor = None
    # 1.创建连接
    @classmethod
    def __get_conn(cls): # 私有方法
        if cls.__conn is None: # 类cls.__conn就是引用变量或对象 #需要先判断连接是否为空,如果为空,再创建;
            cls.__conn = pymysql.connect(host="localhost",
                                         port=3306,
                                         user="root",
                                         password="root",
                                         database="books")
        return cls.__conn # 如果不为空,则直接返回
    # 2.创建游标
    @classmethod
    def __get_cursor(cls):
        if cls.__cursor is None: # 判断游标是否为空
            cls.__cursor = cls.__get_conn().cursor() # 调用连接
        return cls.__cursor
    # 3.执行sql
    @classmethod
    def exe_sql(cls,sql):
        try:
            # 获取游标对象
            cursor = cls.__get_cursor()
            # 调用游标对象.execute(sql)
            cursor.execute(sql)
            # 如果是查询:
            if sql.split()[0].lower() == "select":
                # 返回所有数据
                return cursor.fetchall()
            # 否则:
            else:
                # 提交事务
                cls.__conn.commit()
                # 返回受影响的行数
                return cursor.rowcount
        except Exception as e:
            # 回滚事务
            cls.__conn.rollback()
            # 抛出异常
            print(e)
        finally:
            # 关闭游标
            cls.__close_cursor()
            # 关闭连接
            cls.__close_conn()
    # 4.关闭游标
    @classmethod
    def __close_cursor(cls):
        if cls.__cursor: # 游标也需要判断是否存在
            cls.__cursor.close() # 关闭游标
            cls.__cursor = None # 置空成初始化None状态
    # 5.关闭连接
    @classmethod
    def __close_conn(cls):
        if cls.__conn:
            cls.__conn.close()
            cls.__conn = None

封装使用

from test02_dbutil import DBUtils

# sql = "select * from t_book"
# sql = "insert into t_book(id,title,pub_date) values(5,'东洲列国','2000-02-05');"
# sql = "update t_book set title='哈利波特' where title='东洲列国';"
sql = "delete from t_book where title='哈利波特';"
result = DBUtils.exe_sql(sql)

print(result)

2.Requests库

requests库是用Python语言编写的,基于urllib但比urllib库更加方便,能够完全满足基于HTTP协议的接口测试。

2.1 安装requests库

安装:pip install requests

校验:pip show requests

2.2 请求中常见的数据传递方式

请求中常见的数据传递格式发送数据类型请求方式请求头(Content-Type)说明举例Fiddler请求抓包
字符串params
添加到url的参数
可以传递字典或字典参数
get/浏览器在发送查询字符串时:
1.查询字符(query string)就是附加在URL后,通过?标记参数内容,通常也是键值对的形式
2.对参数内容进行urlencode编码(所有内容以ascii编码呈现)
通过params传递【字典】参数
import requests
respone = requests.get("http://localhost:8080/login",
                        
params={"a":"aaa",
                                "b":123,
                                "c":"验证码"})
print(respone.text)

因为查询字符串不是表单,所以不用设置Content-Type。
GET http://localhost:8080/login?a=aaa&b=123&c=%E9%AA%8C%E8%AF%81%E7%A0%81 HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 0

1.查询字符(query string)就是附加在URL后,通过?标记参数内容,通常也是键值对的形式
2.对参数内容进行urlencode编码(所有内容以ascii编码呈现)
通过params传递【字符串】参数
import requests
respone = requests.get("http://localhost:8080/login",
                        params="a=aaa&b=123&c=验证码")
print(respone.text)
GET http://localhost:8080/login?a=aaa&b=123&c=%E9%AA%8C%E8%AF%81%E7%A0%81 HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
表单datapostapplication/x-www-form-urlencoded浏览器在发送表单数据时,做两件事情:
1.浏览器会添加请求头Content-Type: application/x-www-form-urlencoded
2.对发送的正文进行urlencode编码   %E6%B5%8B%E8%AF%95
import requests
respone = requests.post("http://localhost:8080/login",
                        
data={
                            "a":"aaa",
                            "b":123,
                            "c":"验证码"})
print(respone.text)
POST http://localhost:8080/login HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 41

Content-Type: application/x-www-form-urlencoded

a=aaa&b=123&c=%E9%AA%8C%E8%AF%81%E7%A0%81

1.浏览器会添加请求头Content-Type: application/x-www-form-urlencoded
2.对发送的正文进行urlencode编码   %E6%B5%8B%E8%AF%95
jsonjsonpostapplication/json浏览器在发送json格式的请求时,为了让服务器知道,本次发送的是json字符串,浏览器做了两件事情:
1.添加请求头:Content-Type: application/json
2.将参数进行Unicode编码   \u6d4b\u8bd5

JSON(RFC 8259)规范明确规定:
JSON文本必须使用Unicode编码(默认UTF-8)。
非ASCII字符(如中文)需要以Unicode编码传输,确保任何系统都能正确解析。
import requests
respone = requests.post("http://localhost:8080/login",
                       
 json={
                            "a":"aaa",
                            "b":123,
                            "c":"验证码"})
print(respone.text)
POST http://localhost:8080/login HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Content-Length: 49

Content-Type: application/json

{"a": "aaa", "b": 123, "c": "\u9a8c\u8bc1\u7801"}

1.添加请求头:Content-type: application/json
2.将参数进行Unicode编码   \u6d4b\u8bd5
自定义请求头headersheaders/在requests中,通过headers参数发送自定义请求头:
1.请求头必须是键值对(python中的字典)
2.请求头的内容必须是ascii内容
import requests
headers = {
    "User-Agent": "Mozilla/5.0",
    "token":"tokenxx"
}
respone = requests.post("http://localhost:8080/login",headers=headers,
                        json={
                            "a":"aaa",
                            "b":123,
                            "c":"验证码"})
print(respone.text)
POST http://localhost:8080/login HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive

token: tokenxx
Content-Length: 49
Content-Type: application/json

{"a": "aaa", "b": 123, "c": "\u9a8c\u8bc1\u7801"}
传递Cookie信息cookies/请求头必须是键值对(python中的字典)import requests
cookies = {"session_id": "abc123"}
respone = requests.post("http://localhost:8080/login",cookies=cookies,
                        json={
                            "a":"aaa",
                            "b":123,
                            "c":"验证码"})                
print(respone.text)
POST http://localhost:8080/login HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Cookie: session_id=abc123
Content-Length: 49
Content-Type: application/json

{"a": "aaa", "b": 123, "c": "\u9a8c\u8bc1\u7801"}

2.3响应内容解析

respone.status_code # 状态码
respone.url         # 请求url
respone.encoding    # 响应字符编码
respone.headers     # 响应头
respone.cookies     # cookies信息
respone.text        # 文本形式的响应内容
respone.content     # 字节形式的响应内容
respone.json()      # json形式的响应内容

2.4用Header 或Cookie 传递token

本项目因为服务端要求 token 放在 Header 中,而不是 Cookie 中。代码中通过 headers={"token": token} 明确传递了 token,符合服务端的设计。

(1)Header 传递

import requests
# 登录
login_url="http://localhost:8080/login"
login_data={"username":"jinyong","password":"123456"}
response = requests.post(url=login_url,json=login_data)
print(response.json())
token = response.json().get("data")

# 部门查询
dept_url ="http://localhost:8080/depts"
headers ={
    "token":token
}
response2 = requests.get(url=dept_url,headers=headers)
print(response2.json())

------------------------------------------------------------------------------------------------------

{'code': 1, 'msg': 'success', 'data': 'eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTc0NTg5MTY3MX0.I5RGvyG-VUU6dRfNtlzoWMSIBsaEhDIuuhG8XWtScVU'}
{'code': 1, 'msg': 'success', 'data': [{'id': 1, 'name': '学工部', 'createTime': '2025-03-20T23:29:18', 'updateTime': '2025-03-20T23:29:18'}, {'id': 2, 'name': '教研部', 'createTime': '2025-03-20T23:29:18', 'updateTime': '2025-03-20T23:29:18'}, {'id': 3, 'name': '咨询部', 'createTime': '2025-03-20T23:29:18', 'updateTime': '2025-03-20T23:29:18'}, {'id': 6, 'name': '就业部', 'createTime': '2025-03-21T23:00:47', 'updateTime': '2025-03-21T23:00:47'}, {'id': 7, 'name': '销售部', 'createTime': '2025-03-21T23:02:24', 'updateTime': '2025-03-21T23:02:24'}, {'id': 8, 'name': '食堂部', 'createTime': '2025-04-14T20:11:05', 'updateTime': '2025-04-14T20:11:05'}]}
 

用header传的原因,看fiddler抓包:

GET http://localhost:8080/depts HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
token: eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTc0NTg5MTUyN30.VOKZojjNH2uxUgi6HCyFR5Grq8SDt8gVp1ETLrrW8Pg

(2)Cookie 传递

import requests
# 登录
login_url="http://localhost:8080/login"
login_data={"username":"jinyong","password":"123456"}
response = requests.post(url=login_url,json=login_data)
print(response.json())
token = response.json().get("data")

# 部门查询
dept_url ="http://localhost:8080/depts"
cookies ={
    "token":token
}
response2 = requests.get(url=dept_url,cookies=cookies)
print(response2.json())

------------------------------------------------------------

{'code': 1, 'msg': 'success', 'data': 'eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTc0NTg5MTgxOX0.SPRWKR0AbzPG0eXEdHbjiBw_LG5GgFQJ9WxwjLmbwck'}
{'code': 0, 'msg': 'NOT_LOGIN'}
 

用cookies传的原因,看fiddler抓包:cookies会把Cookise当成键,token整个当做值;

GET http://localhost:8080/depts HTTP/1.1
Host: localhost:8080
User-Agent: python-requests/2.32.3
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Cookie: token=eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTc0NTg5MTgxOX0.SPRWKR0AbzPG0eXEdHbjiBw_LG5GgFQJ9WxwjLmbwck

2.5session跨请求保持cookies

session对象代表一次用户会话:从客户端浏览器连接服务器开始,到客户端浏览器与服务器断开
会话能让我们在跨请求时候保持某些参数,比如在同一个session实例发出的所有请求之间保持
cookie.

服务端返回的 token 是直接放在响应体中的,而不是通过 Cookie 返回。requests.Session 只能自动管理 Cookie,但无法自动处理其他自定义头(如 token)。你需要手动将 token 添加到请求头中。

import requests
# 创建 Session 对象
session = requests.Session()
# 登录
login_url = "http://localhost:8080/login"
login_data = {"username": "jinyong", "password": "123456"}
response = session.post(url=login_url, json=login_data)
print(response.json())

# 从响应中提取 token
token = response.json().get("data")

# 将 token 添加到 Session 的请求头中(关键步骤!)
session.headers.update({"token": token})

# 部门查询(自动携带 token)
dept_url = "http://localhost:8080/depts"
response2 = session.get(url=dept_url)
print(response2.json())

-------------------------------------------------------------------------

{'code': 1, 'msg': 'success', 'data': 'eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTc0NTg5MzI2MH0.jxfQz-yJ3e2S4eCWnFN70ncyYVVt_Au1SeGfyp6Wi-g'}
{'code': 1, 'msg': 'success', 'data': [{'id': 1, 'name': '学工部', 'createTime': '2025-03-20T23:29:18', 'updateTime': '2025-03-20T23:29:18'}, {'id': 2, 'name': '教研部', 'createTime': '2025-03-20T23:29:18', 'updateTime': '2025-03-20T23:29:18'}, {'id': 3, 'name': '咨询部', 'createTime': '2025-03-20T23:29:18', 'updateTime': '2025-03-20T23:29:18'}, {'id': 6, 'name': '就业部', 'createTime': '2025-03-21T23:00:47', 'updateTime': '2025-03-21T23:00:47'}, {'id': 7, 'name': '销售部', 'createTime': '2025-03-21T23:02:24', 'updateTime': '2025-03-21T23:02:24'}, {'id': 8, 'name': '食堂部', 'createTime': '2025-04-14T20:11:05', 'updateTime': '2025-04-14T20:11:05'}]}
 

3.UnitTest

3.1unittest介绍

unittest 是 Python 自带的标准库(内置库),无需额外安装即可直接使用。它提供了编写和运行单元测试的基础框架,设计灵感来源于 Java 的 JUnit。

  1. 内置性
    从 Python 2.1 版本开始,unittest 就被纳入标准库中,因此只需通过 import unittest 即可导入使用。

  2. 核心功能

    • 支持测试用例(TestCase 类)、测试套件(TestSuite)和测试运行器(TestRunner)。

    • 提供断言方法(如 assertEqual()assertTrue() 等)。

    • 支持测试前置(setUp())和后置(tearDown())方法。

  3. 使用场景
    适合中小型项目的单元测试,尤其是需要与 Python 自身生态(如 doctest)深度集成的场景。

示例代码

import unittest

class TestExample(unittest.TestCase):
    def test_addition(self):
        self.assertEqual(1 + 1, 2)

if __name__ == '__main__':
    unittest.main()

对比第三方库(如 pytest

  • 优势unittest 作为内置库,兼容性高,无需依赖第三方工具。

  • 劣势:语法相对冗长,功能扩展性不如 pytest(例如缺少自动参数化测试、更简洁的断言语法)。

如果需要更灵活的测试框架,可以考虑安装第三方库(如 pytest),但 unittest 作为内置选项,始终是开箱即用的可靠选择。

3.2使用UnitTest目的

将接口测试脚本集成到UnitTest单元测试框架中,利用UnitTest的功能来运行接口测试用例。

使用UnitTest框架的目的:

        1)方便管理和维护多个测试用例;

        2)提供丰富的断言方法;

        3)能够生成测试报告;

3.3 UnitTest基本使用

(1)实现思路

登录接口测试用例:

实现思路:

# 导包
# 创建测试类
# 创建测试方法
    # setup
        # 实例化session对象
        # 定义登录接口url地址
    # teardown
        # 关闭 session对象    
   
    # 登录成功
        # 发送登录请求并断言    
  
    # 不输入用户名,账号不存在
        # 发送登录请求并断言    
 
    # 不输入密码,密码错误
        # 发送登录请求并断言

(2)代码实现

# 导包
import requests
import unittest
# 创建测试类
class TliasLogin(unittest.TestCase):# 要继承unittest
# 创建测试方法
    # setup
    def setUp(self) :
        # 实例化session对象
        self.session = requests.Session()
        # 定义登录接口url地址
        self.url_login = "http://localhost:8080/login"

    # teardown
    def tearDown(self):
        # 关闭 session对象
        self.session.close()

    # 登录成功
    def test01_success(self):
        # 发送登录请求并断言
        self.login_data={
            "username":"jinyong",
            "password":"123456"
        }
        headers = {"Content-Type": "application/json"}
        response = self.session.post(url=self.url_login,json=self.login_data,headers=headers)
        print(response.json())

        self.assertEqual(200,response.status_code)
        self.assertEqual(1,response.json().get("code"))
        self.assertEqual("success",response.json().get("msg"))
    # 不输入用户名,账号不存在
    def test02_user_is_not_exist(self):
        # 发送登录请求并断言
        self.login_data = {
            "username": "jinyong1",
            "password": "123456"
        }
        headers = {"Content-Type": "application/json"}
        response = self.session.post(url=self.url_login, json=self.login_data, headers=headers)
        print(response.json())

        self.assertEqual(200, response.status_code)
        self.assertEqual(0, response.json().get("code"))
        self.assertEqual("用户名或密码错误", response.json().get("msg"))

    # 不输入密码,密码错误
    def test03_password_error(self):
        # 发送登录请求并断言
        self.login_data = {
            "username": "jinyong",
            "password": "1234567"
        }
        headers = {"Content-Type": "application/json"}
        response = self.session.post(url=self.url_login, json=self.login_data, headers=headers)
        print(response.json())

        self.assertEqual(200, response.status_code)
        self.assertEqual(0, response.json().get("code"))
        self.assertEqual("用户名或密码错误", response.json().get("msg"))

结果

Ran 3 tests in 0.063s

OK

进程已结束,退出代码0
{'code': 1, 'msg': 'success', 'data': 'eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTc0NTkyMjc4OX0.dPrWXISYz4crf6-Sc9C9Ex9pBDXchSV_xfz81um_7jc'}
{'code': 0, 'msg': '用户名或密码错误', 'data': None}
{'code': 0, 'msg': '用户名或密码错误', 'data': None}
 

(3)生成unittest测试报告

第1步:新建suite测试套件

第2步:新建tools空文件夹及HTMLTestRunner.py文件(直接复制)

第3步:新建report空文件夹

第4步:运行后,report文件夹中自动生成报告,再使用浏览器打开

第1步:新建test06_unittest_tlias.py

import time
# 导包
import unittest
from test05_unittest_tlias import TliasLogin
from tools.HTMLTestRunner import HTMLTestRunner
# 封装测试套件
suite = unittest.TestSuite() # 实例化suite
suite.addTest(unittest.makeSuite(TliasLogin)) # TliasLogin中的用例加到suite中

# 指定报告路径
report = "./report/report-{}.html".format(time.strftime("%Y%m%d-%H%M%S"))
# 打开文件流
with open(report,"wb") as f:
    # 创建HTMLTestRunner运行器
    runner = HTMLTestRunner(f,title="Tlias接口测试")
    # 指定测试套件
    runner.run(suite)

第2步:新建tools文件夹,导入HTMLTestRunner.py

"""
A TestRunner for use with the Python unit testing framework. It
generates a HTML report to show the result at a glance.

The simplest way to use this is to invoke its main method. E.g.

    import unittest
    import HTMLTestRunner

    ... define your tests ...

    if __name__ == '__main__':
        HTMLTestRunner.main()


For more customization options, instantiates a HTMLTestRunner object.
HTMLTestRunner is a counterpart to unittest's TextTestRunner. E.g.

    # output to a file
    fp = file('my_report.html', 'wb')
    runner = HTMLTestRunner.HTMLTestRunner(
                stream=fp,
                title='My unit test',
                description='This demonstrates the report output by HTMLTestRunner.'
                )

    # Use an external stylesheet.
    # See the Template_mixin class for more customizable options
    runner.STYLESHEET_TMPL = '<link rel="stylesheet" href="my_stylesheet.css" type="text/css">'

    # run the test
    runner.run(my_test_suite)


------------------------------------------------------------------------
Copyright (c) 2004-2007, Wai Yip Tung
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

* Redistributions of source code must retain the above copyright notice,
  this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
  notice, this list of conditions and the following disclaimer in the
  documentation and/or other materials provided with the distribution.
* Neither the name Wai Yip Tung nor the names of its contributors may be
  used to endorse or promote products derived from this software without
  specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

# URL: http://tungwaiyip.info/software/HTMLTestRunner.html

__author__ = "Wai Yip Tung"
__version__ = "0.8.2"


"""
Change History

Version 0.8.2
* Show output inline instead of popup window (Viorel Lupu).

Version in 0.8.1
* Validated XHTML (Wolfgang Borgert).
* Added description of test classes and test cases.

Version in 0.8.0
* Define Template_mixin class for customization.
* Workaround a IE 6 bug that it does not treat <script> block as CDATA.

Version in 0.7.1
* Back port to Python 2.3 (Frank Horowitz).
* Fix missing scroll bars in detail log (Podi).
"""

# TODO: color stderr
# TODO: simplify javascript using ,ore than 1 class in the class attribute?

import datetime
import io
import sys
import time
import unittest
from xml.sax import saxutils


# ------------------------------------------------------------------------
# The redirectors below are used to capture output during testing. Output
# sent to sys.stdout and sys.stderr are automatically captured. However
# in some cases sys.stdout is already cached before HTMLTestRunner is
# invoked (e.g. calling logging.basicConfig). In order to capture those
# output, use the redirectors for the cached stream.
#
# e.g.
#   >>> logging.basicConfig(stream=HTMLTestRunner.stdout_redirector)
#   >>>

class OutputRedirector(object):
    """ Wrapper to redirect stdout or stderr """
    def __init__(self, fp):
        self.fp = fp

    def write(self, s):
        self.fp.write(s)

    def writelines(self, lines):
        self.fp.writelines(lines)

    def flush(self):
        self.fp.flush()

stdout_redirector = OutputRedirector(sys.stdout)
stderr_redirector = OutputRedirector(sys.stderr)



# ----------------------------------------------------------------------
# Template

class Template_mixin(object):
    """
    Define a HTML template for report customerization and generation.

    Overall structure of an HTML report

    HTML
    +------------------------+
    |<html>                  |
    |  <head>                |
    |                        |
    |   STYLESHEET           |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </head>               |
    |                        |
    |  <body>                |
    |                        |
    |   HEADING              |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   REPORT               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |   ENDING               |
    |   +----------------+   |
    |   |                |   |
    |   +----------------+   |
    |                        |
    |  </body>               |
    |</html>                 |
    +------------------------+
    """

    STATUS = {
    0: 'pass',
    1: 'fail',
    2: 'error',
    }

    DEFAULT_TITLE = 'Unit Test Report'
    DEFAULT_DESCRIPTION = ''

    # ------------------------------------------------------------------------
    # HTML Template

    HTML_TMPL = r"""<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>%(title)s</title>
    <meta name="generator" content="%(generator)s"/>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    %(stylesheet)s
</head>
<body>
<script language="javascript" type="text/javascript"><!--
output_list = Array();

/* level - 0:Summary; 1:Failed; 2:All */
function showCase(level) {
    trs = document.getElementsByTagName("tr");
    for (var i = 0; i < trs.length; i++) {
        tr = trs[i];
        id = tr.id;
        if (id.substr(0,2) == 'ft') {
            if (level < 1) {
                tr.className = 'hiddenRow';
            }
            else {
                tr.className = '';
            }
        }
        if (id.substr(0,2) == 'pt') {
            if (level > 1) {
                tr.className = '';
            }
            else {
                tr.className = 'hiddenRow';
            }
        }
    }
}


function showClassDetail(cid, count) {
    var id_list = Array(count);
    var toHide = 1;
    for (var i = 0; i < count; i++) {
        tid0 = 't' + cid.substr(1) + '.' + (i+1);
        tid = 'f' + tid0;
        tr = document.getElementById(tid);
        if (!tr) {
            tid = 'p' + tid0;
            tr = document.getElementById(tid);
        }
        id_list[i] = tid;
        if (tr.className) {
            toHide = 0;
        }
    }
    for (var i = 0; i < count; i++) {
        tid = id_list[i];
        if (toHide) {
            document.getElementById('div_'+tid).style.display = 'none'
            document.getElementById(tid).className = 'hiddenRow';
        }
        else {
            document.getElementById(tid).className = '';
        }
    }
}


function showTestDetail(div_id){
    var details_div = document.getElementById(div_id)
    var displayState = details_div.style.display
    // alert(displayState)
    if (displayState != 'block' ) {
        displayState = 'block'
        details_div.style.display = 'block'
    }
    else {
        details_div.style.display = 'none'
    }
}


function html_escape(s) {
    s = s.replace(/&/g,'&amp;');
    s = s.replace(/</g,'&lt;');
    s = s.replace(/>/g,'&gt;');
    return s;
}

/* obsoleted by detail in <div>
function showOutput(id, name) {
    var w = window.open("", //url
                    name,
                    "resizable,scrollbars,status,width=800,height=450");
    d = w.document;
    d.write("<pre>");
    d.write(html_escape(output_list[id]));
    d.write("\n");
    d.write("<a href='javascript:window.close()'>close</a>\n");
    d.write("</pre>\n");
    d.close();
}
*/
--></script>

%(heading)s
%(report)s
%(ending)s

</body>
</html>
"""
    # variables: (title, generator, stylesheet, heading, report, ending)


    # ------------------------------------------------------------------------
    # Stylesheet
    #
    # alternatively use a <link> for external style sheet, e.g.
    #   <link rel="stylesheet" href="$url" type="text/css">

    STYLESHEET_TMPL = """
<style type="text/css" media="screen">
body        { font-family: verdana, arial, helvetica, sans-serif; font-size: 80%; }
table       { font-size: 100%; }
pre         { }

/* -- heading ---------------------------------------------------------------------- */
h1 {
	font-size: 16pt;
	color: gray;
}
.heading {
    margin-top: 0ex;
    margin-bottom: 1ex;
}

.heading .attribute {
    margin-top: 1ex;
    margin-bottom: 0;
}

.heading .description {
    margin-top: 4ex;
    margin-bottom: 6ex;
}

/* -- css div popup ------------------------------------------------------------------------ */
a.popup_link {
}

a.popup_link:hover {
    color: red;
}

.popup_window {
    display: none;
    position: relative;
    left: 0px;
    top: 0px;
    /*border: solid #627173 1px; */
    padding: 10px;
    background-color: #E6E6D6;
    font-family: "Lucida Console", "Courier New", Courier, monospace;
    text-align: left;
    font-size: 8pt;
    width: 500px;
}

}
/* -- report ------------------------------------------------------------------------ */
#show_detail_line {
    margin-top: 3ex;
    margin-bottom: 1ex;
}
#result_table {
    width: 80%;
    border-collapse: collapse;
    border: 1px solid #777;
}
#header_row {
    font-weight: bold;
    color: white;
    background-color: #777;
}
#result_table td {
    border: 1px solid #777;
    padding: 2px;
}
#total_row  { font-weight: bold; }
.passClass  { background-color: #6c6; }
.failClass  { background-color: #c60; }
.errorClass { background-color: #c00; }
.passCase   { color: #6c6; }
.failCase   { color: #c60; font-weight: bold; }
.errorCase  { color: #c00; font-weight: bold; }
.hiddenRow  { display: none; }
.testcase   { margin-left: 2em; }


/* -- ending ---------------------------------------------------------------------- */
#ending {
}

</style>
"""



    # ------------------------------------------------------------------------
    # Heading
    #

    HEADING_TMPL = """<div class='heading'>
<h1>%(title)s</h1>
%(parameters)s
<p class='description'>%(description)s</p>
</div>

""" # variables: (title, parameters, description)

    HEADING_ATTRIBUTE_TMPL = """<p class='attribute'><strong>%(name)s:</strong> %(value)s</p>
""" # variables: (name, value)



    # ------------------------------------------------------------------------
    # Report
    #

    REPORT_TMPL = """
<p id='show_detail_line'>Show
<a href='javascript:showCase(0)'>Summary</a>
<a href='javascript:showCase(1)'>Failed</a>
<a href='javascript:showCase(2)'>All</a>
</p>
<table id='result_table'>
<colgroup>
<col align='left' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
<col align='right' />
</colgroup>
<tr id='header_row'>
    <td>Test Group/Test case</td>
    <td>Count</td>
    <td>Pass</td>
    <td>Fail</td>
    <td>Error</td>
    <td>View</td>
</tr>
%(test_list)s
<tr id='total_row'>
    <td>Total</td>
    <td>%(count)s</td>
    <td>%(Pass)s</td>
    <td>%(fail)s</td>
    <td>%(error)s</td>
    <td>&nbsp;</td>
</tr>
</table>
""" # variables: (test_list, count, Pass, fail, error)

    REPORT_CLASS_TMPL = r"""
<tr class='%(style)s'>
    <td>%(desc)s</td>
    <td>%(count)s</td>
    <td>%(Pass)s</td>
    <td>%(fail)s</td>
    <td>%(error)s</td>
    <td><a href="javascript:showClassDetail('%(cid)s',%(count)s)">Detail</a></td>
</tr>
""" # variables: (style, desc, count, Pass, fail, error, cid)


    REPORT_TEST_WITH_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'>

    <!--css div popup start-->
    <a class="popup_link" onfocus='this.blur();' href="javascript:showTestDetail('div_%(tid)s')" >
        %(status)s</a>

    <div id='div_%(tid)s' class="popup_window">
        <div style='text-align: right; color:red;cursor:pointer'>
        <a onfocus='this.blur();' onclick="document.getElementById('div_%(tid)s').style.display = 'none' " >
           [x]</a>
        </div>
        <pre>
        %(script)s
        </pre>
    </div>
    <!--css div popup end-->

    </td>
</tr>
""" # variables: (tid, Class, style, desc, status)


    REPORT_TEST_NO_OUTPUT_TMPL = r"""
<tr id='%(tid)s' class='%(Class)s'>
    <td class='%(style)s'><div class='testcase'>%(desc)s</div></td>
    <td colspan='5' align='center'>%(status)s</td>
</tr>
""" # variables: (tid, Class, style, desc, status)


    REPORT_TEST_OUTPUT_TMPL = r"""
%(id)s: %(output)s
""" # variables: (id, output)



    # ------------------------------------------------------------------------
    # ENDING
    #

    ENDING_TMPL = """<div id='ending'>&nbsp;</div>"""

# -------------------- The end of the Template class -------------------


TestResult = unittest.TestResult

class _TestResult(TestResult):
    # note: _TestResult is a pure representation of results.
    # It lacks the output and reporting ability compares to unittest._TextTestResult.

    def __init__(self, verbosity=1):
        TestResult.__init__(self)
        self.stdout0 = None
        self.stderr0 = None
        self.success_count = 0
        self.failure_count = 0
        self.error_count = 0
        self.verbosity = verbosity

        # result is a list of result in 4 tuple
        # (
        #   result code (0: success; 1: fail; 2: error),
        #   TestCase object,
        #   Test output (byte string),
        #   stack trace,
        # )
        self.result = []


    def startTest(self, test):
        TestResult.startTest(self, test)
        # just one buffer for both stdout and stderr
        self.outputBuffer = io.StringIO()
        stdout_redirector.fp = self.outputBuffer
        stderr_redirector.fp = self.outputBuffer
        self.stdout0 = sys.stdout
        self.stderr0 = sys.stderr
        sys.stdout = stdout_redirector
        sys.stderr = stderr_redirector


    def complete_output(self):
        """
        Disconnect output redirection and return buffer.
        Safe to call multiple times.
        """
        if self.stdout0:
            sys.stdout = self.stdout0
            sys.stderr = self.stderr0
            self.stdout0 = None
            self.stderr0 = None
        return self.outputBuffer.getvalue()


    def stopTest(self, test):
        # Usually one of addSuccess, addError or addFailure would have been called.
        # But there are some path in unittest that would bypass this.
        # We must disconnect stdout in stopTest(), which is guaranteed to be called.
        self.complete_output()


    def addSuccess(self, test):
        self.success_count += 1
        TestResult.addSuccess(self, test)
        output = self.complete_output()
        self.result.append((0, test, output, ''))
        if self.verbosity > 1:
            sys.stderr.write('ok ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('.')

    def addError(self, test, err):
        self.error_count += 1
        TestResult.addError(self, test, err)
        _, _exc_str = self.errors[-1]
        output = self.complete_output()
        self.result.append((2, test, output, _exc_str))
        if self.verbosity > 1:
            sys.stderr.write('E  ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('E')

    def addFailure(self, test, err):
        self.failure_count += 1
        TestResult.addFailure(self, test, err)
        _, _exc_str = self.failures[-1]
        output = self.complete_output()
        self.result.append((1, test, output, _exc_str))
        if self.verbosity > 1:
            sys.stderr.write('F  ')
            sys.stderr.write(str(test))
            sys.stderr.write('\n')
        else:
            sys.stderr.write('F')


class HTMLTestRunner(Template_mixin):
    """
    """
    def __init__(self, stream=sys.stdout, verbosity=1, title=None, description=None):
        self.stream = stream
        self.verbosity = verbosity
        if title is None:
            self.title = self.DEFAULT_TITLE
        else:
            self.title = title
        if description is None:
            self.description = self.DEFAULT_DESCRIPTION
        else:
            self.description = description

        self.startTime = datetime.datetime.now()


    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, '\nTimeElapsed: %s' % (self.stopTime-self.startTime))
        return result


    def sortResult(self, result_list):
        # unittest does not seems to run in any particular order.
        # Here at least we want to group them together by class.
        rmap = {}
        classes = []
        for n,t,o,e in result_list:
            cls = t.__class__
            if not cls in rmap:
                rmap[cls] = []
                classes.append(cls)
            rmap[cls].append((n,t,o,e))
        r = [(cls, rmap[cls]) for cls in classes]
        return r


    def getReportAttributes(self, result):
        """
        Return report attributes as a list of (name, value).
        Override this to add custom attributes.
        """
        startTime = str(self.startTime)[:19]
        duration = str(self.stopTime - self.startTime)
        status = []
        if result.success_count: status.append('Pass %s'    % result.success_count)
        if result.failure_count: status.append('Failure %s' % result.failure_count)
        if result.error_count:   status.append('Error %s'   % result.error_count  )
        if status:
            status = ' '.join(status)
        else:
            status = 'none'
        return [
            ('Start Time', startTime),
            ('Duration', duration),
            ('Status', status),
        ]


    def generateReport(self, test, result):
        report_attrs = self.getReportAttributes(result)
        generator = 'HTMLTestRunner %s' % __version__
        stylesheet = self._generate_stylesheet()
        heading = self._generate_heading(report_attrs)
        report = self._generate_report(result)
        ending = self._generate_ending()
        output = self.HTML_TMPL % dict(
            title = saxutils.escape(self.title),
            generator = generator,
            stylesheet = stylesheet,
            heading = heading,
            report = report,
            ending = ending,
        )
        self.stream.write(output.encode('utf8'))


    def _generate_stylesheet(self):
        return self.STYLESHEET_TMPL


    def _generate_heading(self, report_attrs):
        a_lines = []
        for name, value in report_attrs:
            line = self.HEADING_ATTRIBUTE_TMPL % dict(
                    name = saxutils.escape(name),
                    value = saxutils.escape(value),
                )
            a_lines.append(line)
        heading = self.HEADING_TMPL % dict(
            title = saxutils.escape(self.title),
            parameters = ''.join(a_lines),
            description = saxutils.escape(self.description),
        )
        return heading


    def _generate_report(self, result):
        rows = []
        sortedResult = self.sortResult(result.result)
        for cid, (cls, cls_results) in enumerate(sortedResult):
            # subtotal for a class
            np = nf = ne = 0
            for n,t,o,e in cls_results:
                if n == 0: np += 1
                elif n == 1: nf += 1
                else: ne += 1

            # format class description
            if cls.__module__ == "__main__":
                name = cls.__name__
            else:
                name = "%s.%s" % (cls.__module__, cls.__name__)
            doc = cls.__doc__ and cls.__doc__.split("\n")[0] or ""
            desc = doc and '%s: %s' % (name, doc) or name

            row = self.REPORT_CLASS_TMPL % dict(
                style = ne > 0 and 'errorClass' or nf > 0 and 'failClass' or 'passClass',
                desc = desc,
                count = np+nf+ne,
                Pass = np,
                fail = nf,
                error = ne,
                cid = 'c%s' % (cid+1),
            )
            rows.append(row)

            for tid, (n,t,o,e) in enumerate(cls_results):
                self._generate_report_test(rows, cid, tid, n, t, o, e)

        report = self.REPORT_TMPL % dict(
            test_list = ''.join(rows),
            count = str(result.success_count+result.failure_count+result.error_count),
            Pass = str(result.success_count),
            fail = str(result.failure_count),
            error = str(result.error_count),
        )
        return report


    def _generate_report_test(self, rows, cid, tid, n, t, o, e):
        # e.g. 'pt1.1', 'ft1.1', etc
        has_output = bool(o or e)
        tid = (n == 0 and 'p' or 'f') + 't%s.%s' % (cid+1,tid+1)
        name = t.id().split('.')[-1]
        doc = t.shortDescription() or ""
        desc = doc and ('%s: %s' % (name, doc)) or name
        tmpl = has_output and self.REPORT_TEST_WITH_OUTPUT_TMPL or self.REPORT_TEST_NO_OUTPUT_TMPL

        # o and e should be byte string because they are collected from stdout and stderr?
        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
        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
        else:
            ue = e

        script = self.REPORT_TEST_OUTPUT_TMPL % dict(
            id = tid,
            output = saxutils.escape(uo+ue),
        )

        row = tmpl % dict(
            tid = tid,
            Class = (n == 0 and 'hiddenRow' or 'none'),
            style = n == 2 and 'errorCase' or (n == 1 and 'failCase' or 'none'),
            desc = desc,
            script = script,
            status = self.STATUS[n],
        )
        rows.append(row)
        if not has_output:
            return

    def _generate_ending(self):
        return self.ENDING_TMPL


##############################################################################
# Facilities for running tests from the command line
##############################################################################

# Note: Reuse unittest.TestProgram to launch test. In the future we may
# build our own launcher to support more specific command line
# parameters like test title, CSS, etc.
class TestProgram(unittest.TestProgram):
    """
    A variation of the unittest.TestProgram. Please refer to the base
    class for command line parameters.
    """
    def runTests(self):
        # Pick HTMLTestRunner as the default test runner.
        # base class's testRunner parameter is not useful because it means
        # we have to instantiate HTMLTestRunner before we know self.verbosity.
        if self.testRunner is None:
            self.testRunner = HTMLTestRunner(verbosity=self.verbosity)
        unittest.TestProgram.runTests(self)

main = TestProgram

##############################################################################
# Executing this module from the command line
##############################################################################

if __name__ == "__main__":
    main(module=None)

第3步:新建空的report文件夹

第4步:使用浏览器打开报告 

3.4 unittest参数化【重点】

上面测试用例部分,比如断言都是重复的,所以需参数化;

第1步:改成传参:

第2步:导包 parameterized(安装pip install parameterized

第3步:新建构造测试数据

第4步:装饰器,导入构造测试数据方法,返回获取外部参数,供下面方法使用

第5步:新建data文件夹,造测试数据login.json

第6步:完善构造测试数据方法【重点】

第7步:测试套件suite中重新引入,进行测试

第1-4步:

第5步,新建data文件夹,新建测试数据login.json文件

[
  {
    "desc":"登录成功",
    "username":"jinyong",
    "password":"123456",
    "status_code":200,
    "code":1,
    "msg":"success"
  },
   {
    "desc":"账号不存在",
    "username":"jinyong1",
    "password":"123456",
    "status_code":200,
    "code":0,
    "msg":"用户名或密码错误"
  },
   {
    "desc":"密码错误",
    "username":"jinyong",
    "password":"1234567",
    "status_code":200,
    "code":0,
    "msg":"用户名或密码错误"
  }
]

第6步,完善构造测试数据方法

# 构造测试数据
def build_data():
    test_data=[]
    file = "./data/login.json"
    with open(file,encoding="utf-8") as f:
        json_data = json.load(f) # json数据,读取到json_data中
        for case_data in json_data:
            username = case_data.get("username")
            password = case_data.get("password")
            status_code = case_data.get("status_code")
            code = case_data.get("code")
            msg = case_data.get("msg")
            test_data.append((username,password,status_code,code,msg)) # 以元组方式追加,追加到列表中
    print("test_data=",test_data)
    return test_data # 有了参数化以后,不能在类里面执行,需要在类外面执行;

第7步:测试套件suite中重新引入,进行测试

import time
# 导包
import unittest
# from test05_unittest_tlias import TliasLogin
from test07_unittest_params import TliasLogin
from tools.HTMLTestRunner import HTMLTestRunner
# 封装测试套件
suite = unittest.TestSuite() # 实例化suite
suite.addTest(unittest.makeSuite(TliasLogin)) # TliasLogin中的用例加到suite中

# 指定报告路径
report = "./report/report-{}.html".format(time.strftime("%Y%m%d-%H%M%S"))
# 打开文件流
with open(report,"wb") as f:
    # 创建HTMLTestRunner运行器
    runner = HTMLTestRunner(f,title="Tlias接口测试")
    # 指定测试套件
    runner.run(suite)

运行结果:

# End

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值