目录
接口测试小项目
这周我完成了IHRM人力资源管理项目其中的登录接口和员工管理的测试用例的编写, 使用Python, Request和Unittest框架完成了接口自动化测试,生成测试报告。最后,使用Jenkins进行了可持续集成测试。 不足之处希望大佬指正!
1. 说明
1.1 项目介绍
IHRM人力资源管理系统包含组织管理, 员工管理, 招聘管理,劳动合同等等功能模块。 技术栈包含
前端:
- 以Node.js为核心的Vue.js前端技术生态架构
后端:
- SpringBoot+SpringCloud+SpringMVC+SpringData (Spring全家桶)
- MySQL + Redis + RabbitMQ
其中完整的pi文档链接:接口文档
1.2 项目架构
架构说明
- 因为我没有写从调用数据库的工具类在这个小项目里。所以TestCase和数据库交互的部分我打了叉。
- 核心在于将测试用例与被测试系统API进行分离,便于后期维护
- 测试用例是通过unittest进行管理,并提供了丰富的断言(等于、包含等)
- 可以通过参数化思想测试数据与测试脚本的分离
- 借助第三方工具快速的生成HTML报告
1.3 项目目录结构
这个小项目链接: interface_project2
项目目录结构如下所示
- interface_project2 – 项目代号
- data – 管理测试数据的文件夹
- report – 管理测试结果报告的文件夹
- api – 封装被测试系统的接口
- scripts – 测试用例脚本
- tools – 第三方工具包管理
- app.py – 配置信息文件
- run_suite.py – 测试用例执行入口
- utils.py – 自定义工具类
1.4 IHRM项目API文档解析
IHRM项目的部分API文档解析如下图所示:
2. 代码实现
2.1 封装被测试系统的接口
登录接口login.py
# 导包
import requests
"""
被测系统的接口封装
登录: "http://ihrm2-test.itheima.net/api/sys/login"
"""
# 定义接口类
class LoginAPI:
# 初始化
def __init__(self):
self.login_url = "http://ihrm2-test.itheima.net/api/sys/login"
# 登录接口
def login(self, login_data):
# 这里不是表单而是json
return requests.post(url=self.login_url, json=login_data)
if __name__ == '__main__':
response = LoginAPI().login({"mobile": "13800000002", "password": "123456"})
print(response.json())
员工管理接口employee.py
# 导包
import app
import requests
# 创建接口类
class EmployeeAPI:
# 初始化
def __init__(self):
self.url_add_employee = app.BASE_URL + "/api/sys/user"
self.url_update_employee = app.BASE_URL + "/api/sys/user/{}"
self.url_get_employee = app.BASE_URL + "/api/sys/user/{}"
self.url_delete_employee = app.BASE_URL + "/api/sys/user/{}"
# 员工添加
def add_employee(self, add_employee_data):
return requests.post(url=self.url_add_employee, json=add_employee_data, headers=app.headers_data)
# 员工修改
def update_employee(self, employee_id, update_data):
url = self.url_update_employee.format(employee_id)
return requests.put(url=url, json=update_data, headers=app.headers_data)
# 员工查询
def get_employee(self, employee_id):
url = self.url_update_employee.format(employee_id)
return requests.get(url=url, headers=app.headers_data)
# 员工删除
def delete_employee(self, employee_id):
url = self.url_update_employee.format(employee_id)
return requests.delete(url=url, headers=app.headers_data)
2.2 参数化数据
这里我准备了13条用户登录的测试用例, 存入data文件夹下的login.json
文件, 方便等会我们用uniitest对它进行登录接口测试参数化。
[
{
"desc": "case001 登录成功",
"login_data": {
"mobile": "13800000002",
"password": "123456"
},
"status_code": 200,
"success": true,
"code": 10000,
"message": "操作成功"
},
{
"desc": "case002 不输入手机号",
"login_data": {
"mobile": "",
"password": "123456"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case003 不输入密码",
"login_data": {
"mobile": "13800000002",
"password": ""
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case004 手机长度小于11位",
"login_data": {
"mobile": "1380000000",
"password": "123456"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case005 手机场地大于11位",
"login_data": {
"mobile": "138000000023",
"password": "123456"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case006 手机号输入非数字",
"login_data": {
"mobile": "error",
"password": "123456"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case007 输入未注册手机号",
"login_data": {
"mobile": "13812321236",
"password": "123456"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case008 多参",
"login_data": {
"mobile": "13800000002",
"password": "123456",
"haha": "xixi"
},
"status_code": 200,
"success": true,
"code": 10000,
"message": "操作成功"
},
{
"desc": "case009 少参-缺少手机号",
"login_data": {
"password": "123456"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case010 少参-缺少密码",
"login_data": {
"mobile": "13800000002"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case011 无参",
"status_code": 200,
"success": false,
"code": 99999,
"message": "抱歉,系统繁忙"
},
{
"desc": "case012 错误参数-手机号",
"login_data": {
"mobiel": "13800000002",
"password": "123456"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
},
{
"desc": "case013 错误参数-密码",
"login_data": {
"mobile": "13800000002",
"passwd": "123456"
},
"status_code": 200,
"success": false,
"code": 20001,
"message": "用户名或密码错误"
}
]
2.3 Unittest测试
test01_login.py
中我定义了uniitest测试类, 手动输入了13条测试用例,没有进行参数化。
# 导包
import app
import unittest
from api.login import LoginAPI
from utils import common_assert
# 创建测试类
class TestLogin(unittest.TestCase):
# 前置处理
def setUp(self):
self.login_api = LoginAPI()
# 定义测试用例
# case001 登录成功
def test01_case001(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "13800000002", "password": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, True, 10000, "操作成功")
# 提取token信息
app.TOKEN = "Bearer " + response.json().get("data")
print(app.TOKEN)
app.headers_data["Authorization"] = app.TOKEN
print(app.headers_data["Authorization"])
# case002 不输入手机号
def test02_case002(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "", "password": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case003 不输入密码
def test03_case003(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "13800000002", "password": ""})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case004 手机长度小于11位
def test04_case004(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "1380000000", "password": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case005 手机场地大于11位
def test05_case005(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "138000000023", "password": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case006 手机号输入非数字
def test06_case006(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "error", "password": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case007 输入未注册手机号
def test07_case007(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "13812321236", "password": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case008 多参
def test08_case008(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "13800000002", "password": "123456", "haha": "xixi"})
print(response.json())
# 断言
common_assert(self, response, 200, True, 10000, "操作成功")
# case009 少参-缺少手机号
def test09_case009(self):
# 调用登录接口进行登录
response = self.login_api.login({"password": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case010 少参-缺少密码
def test10_case010(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "13800000002"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case011 无参
def test11_case011(self):
# 调用登录接口进行登录
response = self.login_api.login(None)
print(response.json())
# 断言
common_assert(self, response, 200, False, 99999, "抱歉,系统繁忙")
# case012 错误参数-手机号
def test12_case012(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobiel": "13800000002", "password": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
# case013 错误参数-密码
def test13_case013(self):
# 调用登录接口进行登录
response = self.login_api.login({"mobile": "13800000002", "passwd": "123456"})
print(response.json())
# 断言
common_assert(self, response, 200, False, 20001, "用户名或密码错误")
test02_login_params.py
相比test01_login.py
实现了参数化,其中buid_data
函数 将读取的json数据传入一个字典。
# 导包
import json
import unittest
from api.login import LoginAPI
from parameterized import parameterized
# 构建测试数据
def build_data():
# 指定文件路径
json_file = "../data/login.json"
# 打开json文件
test_data = []
with open(json_file, encoding="utf-8") as f:
json_data = json.load(f)
for case_data in json_data:
login_data = case_data.get("login_data")
status_code = case_data.get("status_code")
success = case_data.get("success")
code = case_data.get("code")
message = case_data.get("message")
test_data.append((login_data, status_code, success, code, message))
print("test_data = {}".format((login_data, status_code, success, code, message)))
return test_data
# 创建测试类
class TestLogin(unittest.TestCase):
# 前置处理
def setUp(self):
self.login_api = LoginAPI()
@parameterized.expand(build_data)
# 定义测试用例
def test01_login(self, login_data, status_code, success, code, message):
# 调用登录接口进行登录
response = self.login_api.login(login_data)
print(response.json())
# 断言
self.assertEqual(status_code, response.status_code)
self.assertEqual(success, response.json().get("success"))
self.assertEqual(code, response.json().get("code"))
self.assertIn(message, response.json().get("message"))
test03_employee
是员工管理模块的uniitest测试类, 测试类包含添加员工, 修改员工, 删除员工和查询员工。
# 导包
import unittest
from api.employee import EmployeeAPI
from utils import common_assert
# 创建测试类
class TestEmployee(unittest.TestCase):
employee_id = None
# employee_id = 1288061322868658176
# 前置处理
def setUp(self):
self.employee_api = EmployeeAPI()
# 添加员工测试用例设计
def test01_add_employee(self):
add_employee_data = {
"username": "davi0709t0728066", # 用户名唯一
"mobile": "66612072866", # 手机号唯一
"timeOfEntry": "2020-07-09",
"formOfEmployment": 1,
"workNumber": "072866", # 员工ID唯一性
"departmentName": "销售",
"departmentId": "1266699057968001024",
"correctionTime": "2020-07-30T16:00:00.000Z"
}
# 获取响应结果
response = self.employee_api.add_employee(add_employee_data=add_employee_data)
print(response.json())
# 断言
# self.assertEqual(200, response.status_code)
# self.assertEqual(True, response.json().get("success"))
# self.assertEqual(10000, response.json().get("code"))
# self.assertIn("操作成功", response.json().get("message"))
# common_assert(self, response, 200, True, 10000, "操作成功")
common_assert(self, response) # 直接使用公共断言方法的默认值即可
# 提取员工ID
TestEmployee.employee_id = response.json().get("data").get("id")
print(TestEmployee.employee_id)
# 修改员工测试用例设计
def test02_update_employee(self):
update_employee_data = {"username": "rose0728"}
# 获取响应结果
response = self.employee_api.update_employee(TestEmployee.employee_id, update_data=update_employee_data)
print(response.json())
# 断言
common_assert(self, response) # 直接使用公共断言方法的默认值即可
# 查询员工测试用例设计
def test03_get_employee(self):
# 获取响应结果
response = self.employee_api.get_employee(TestEmployee.employee_id)
print(response.json())
# 断言
common_assert(self, response) # 直接使用公共断言方法的默认值即可
# 删除员工测试用例设计
def test04_delete_employee(self):
# 获取响应结果
response = self.employee_api.delete_employee(TestEmployee.employee_id)
print(response.json())
# 断言
common_assert(self, response) # 直接使用公共断言方法的默认值即可
2.4 工具
为了生成HTML报告,我这里直接引用了一个别已经写好的模块,并将它放在tools文件夹下, 等会我们可以直接在runner_suite.py里直接导入。
生成HTML报告链接: HTMLTestRunner.py
2.5 执行和其他
app.py
"""
这个文件夹放的是配置信息
"""
# 项目基础URL -- 测试环境
BASE_URL = "http://ihrm2-test.itheima.net"
# token
TOKEN = None
# 请求头数据
headers_data = {
"Content-Type": "application/json",
"Authorization": "Bearer bcfc9d2e-a575-4240-9569-f5f860619f97"
}
utils.py
# 公共断言方法
def common_assert(case, response, status_code=200, success=True, code=10000, message="操作成功"):
case.assertEqual(status_code, response.status_code)
case.assertEqual(success, response.json().get("success"))
case.assertEqual(code, response.json().get("code"))
case.assertIn(message, response.json().get("message"))
run_suite.py
生成测试报告
# 导包
import time
import unittest
from scripts.test01_login import TestLogin
from scripts.test02_login_params import TestLogin2
from scripts.test03_employee import TestEmployee
from tools.HTMLTestRunner import HTMLTestRunner
# 组装测试套件
suite = unittest.TestSuite()
# 登录接口测试用例
suite.addTest(unittest.makeSuite(TestLogin))
# 登录接口测试用例参数化
suite.addTest(unittest.makeSuite(TestLogin2))
# 员工管理场景测试用例
suite.addTest(TestLogin("test01_case001"))
suite.addTest(unittest.makeSuite(TestEmployee))
# 指定测试报告的路径
# report = "./report/report-{}.html".format(time.strftime("%Y%m%d-%H%M%S"))
report = "./report/report.html"
# 打开文件流
with open(report, "wb") as f:
# 创建HTMLTestRunner运行器
runner = HTMLTestRunner(f, title="API Report")
# 执行测试套件
runner.run(suite)
生成的报告截图如下图所示:
3. 可持续集成
我使用Jenkins对该项目进行了可持续集成测试。Jenkins及其插件的安装说明, 可以参考这个下载链接: Jenkins资料, 提取码:xzgk。
3.1 具体操作
-
进入jenkins安装包所在目录(如D:\jenkins),打开一个dos窗口,运行命令
java -jar jenkins.war
, 如果出现下图所示的 Jenkins is fully up and running, 说明jenkins已经正常启动。
-
注意
java -jar jenkins.war
默认端口是8080, 在浏览器中输入localhost:8080
来进入Jenkins的登录界面, 然后输入你的账号和密码,成功登录后我们开始创建我们的Freestyle project
-
点击New Item 创建一个新的项目
然后选择 项目的类型为Freestyle project
-
Source Code Management选择Git, 属于项目的URL和你Gitee的登录信息
-
Build这里选择使用
Windows bash comand
输入命令如下:call python run_suite.py exit 0
-
接下来就是生成HTML报告和邮件通知。 要注意的是
Index page
必须填写 report.html, 这里必须与run_suite.py 代码里面一致。 Eiditable Email Notifications 这里的设置比较复杂,这里就不细说了。
-
点击
Build now
运行这个项目, 在排队的项目中点击你当前运行的项目。如果运行成功, 你能看到下图Finished: SUCCESS
的状态
最后你可以在下列路径找到刚刚生成的HTML文件
你也可以HTML Report
直接点击, 直接查看但是很有可能会乱码。 所以这里建议先退出Jenkins,使用java -Dhudson.model.DirectoryBrowserSupport.CSP= -jar jenkins.war
启动Jenkins,重复上述所有的流程。这样就可以解决乱码的情况。