python自动化(七)自动化测试平台开发:6.后端开发之用例报告的上传

1.用例报告的上传

1.1 接口分析

  • 接口功能:该接口实现将执行用例时生成的用例报告上传到服务器储存起来,用户可以通过界面查看报告。
  • 接口场景:当用户调用执行用例接口后,用例开始执行,当用例执行完成后,Jenkins任务中会调用上传用例报告的接口,将测试报告上传到服务器上。
    在这里插入图片描述

1.2 添加用例报告表

我们需要在数据库中创建一个报告表,用于存放测试报告的相关信息。且该报告表需为用例表的外键表,与用例表建立多对一的关联关系。即一个用例可以有多个报告。

修改models.py文件如下:

"""
使用flask_sqlalchemy模块声明数据库表
"""
from datetime import datetime
from flask_sqlalchemy import SQLAlchemy

from exit import db

class TestCaseModel(db.Model):
    """
    用例表模型类
    """

    __tablename__ = 'testcase'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String(30), unique=True, nullable=False)
    description = db.Column(db.String(200), unique=False, nullable=True)
    file_path = db.Column(db.String(200), unique=True, nullable=False)
    update_time = db.Column(db.DateTime, default=datetime.now())
    jenkins_last_build_number = db.Column(db.Integer, nullable=True, unique=True)

class CaseReportModel(db.Model):
    """
    用例报告表模型类
    """
    __tablename__ = 'caseerport'
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    file_path = db.Column(db.String(200), unique=True, nullable=False)
    # 设置用例表的id为报告表的外键
    caseid = db.Column(db.Integer,db.ForeignKey("testcase.id",ondelete='RESTRICT'),nullable=False)
    # 将用例表与报告表关联起来,这样我们使用CaseReportModel().test_case可以找到与该报告关联的用例对象。
    # 使用TestCaseModel().test_case_reports,可以找到与该用例关联的所有报告对象列表
    test_case = db.relationship('TestCaseModel',backref='test_case_reports',lazy=True)

1.3 迁移数据库文件

第一步:删除项目的数据表初始化的迁移文件migrates目录

第二步:删除记录迁移版本的数据表alembic_version

第三步:依次在当前目录的终端执行命令:

python manage.py db init
python manage.py db migrate 
python manage.py db upgrade

1.4 完成用例报告上传接口

上面我们已经分析过该接口,该接口的请求中有两个参数。一个为case_id,即用例id;第二个为report_file,即为用例报告文件。该接口中需要将用例报告上传到服务器中储存起来。

第一步:修改forms.py文件,增加用例报告上传接口参数的校验器

from wtforms import Form, StringField, FileField, IntegerField
from wtforms.validators import Length, InputRequired, ValidationError
from flask_wtf.file import FileRequired


# 校验用例储存请求参数
class TestCaseForm(Form):
    name = StringField(validators=[Length(max=30, message='用例名字不合法,参数长度不能大于30'),
                                   InputRequired(message='用例名字不能为空')
                                   ])
    description = StringField(validators=[Length(max=200, message='用例描述不合法,参数长度不能大于200')])
    test_file = FileField(validators=[FileRequired(message='必须上传用例文件')])

    def validate_test_file(self, field):
        """自定义test_file验证器"""
        if not field.data.filename.endswith('.py'): # field.data获取需要校验的参数值,这里为上传的文件对象
            raise ValidationError('用例文件必须为python文件') # 使用ValidationError抛出提示信息

# 校验用例查看请求参数
class GetTestCaseForm(Form):
    id = IntegerField(validators=[InputRequired('用例id不能为空')])

# 校验用例下载请求参数
class DownloadTestCaseForm(Form):
    id = IntegerField(validators=[InputRequired('用例id不能为空')])

# 校验用例执行请求参数
class RunTestCaseForm(Form):
    id = IntegerField(validators=[InputRequired('用例id不能为空')])

# 校验用例状态查询请求参数
class GetTestCaseStatusForm(Form):
    id = IntegerField(validators=[InputRequired('用例id不能为空')])

# 校验上传用例报告请求参数
class CaseReportForm(Form):
    case_id = IntegerField(validators=[InputRequired('用例id不能为空')])
    report_file = FileField(validators=[FileRequired(message='必须上传用例报告文件')])

第二步:修改app.py文件,完成上传用例报告的接口CaseReportView

import os
from datetime import datetime
from flask import Flask, request, jsonify, send_from_directory
import conf
from flask_restful import Api, Resource
from exit import db
from forms import TestCaseForm, GetTestCaseForm, DownloadTestCaseForm, RunTestCaseForm
from models import TestCaseModel
from werkzeug.datastructures import CombinedMultiDict
from jenkinsapi.jenkins import Jenkins
from jenkinsapi import api

# 实例化一个Flask对象,用于启动flask服务
app = Flask(__name__)

# 添加配置文件
app.config.from_object(conf)

# 实例化flask_restful对象
api_ = Api(app)

# 将SQLAlchemy对象db关联到app
db.init_app(app)


# 设置用例储存视图函数
class TestCaseStoreView(Resource):
    def post(self):

        now = datetime.now().strftime('%Y-%m-%d-%H%M%S')  # 生成时间戳
        now_path = os.getcwd()  # 获取当前目录路径

        # 实例化TestCaseForm对象,用于参数校验。其参数为需要校验的参数字典列表
        # CombinedMultiDict将两个属性组合起来传给校验器
        form = TestCaseForm(CombinedMultiDict([request.form,request.files]))


        if form.validate():

            # 参数校验通过
            name = form.name.data # 获取参数name的值
            description = form.description.data # 获取参数description的值
            test_file = form.test_file.data # 获取用例文件test_file对象

            # 判断name是否已在数据库中存在
            name_fag = db.session.query(TestCaseModel).filter(TestCaseModel.name==name).first()
            if name_fag is  None:
                if description is  not None:
                    # 保存用例文件
                    file_path = os.path.join(now_path, f'./case_files/{
       now}_{
       test_file.filename}')
                    test_file.save(file_path)

                    # 保存数据库
                    testcase = TestCaseModel(name=name,description=description,file_path=file_path)
                    db.session.add(testcase)
                    db.session.commit()

                    res = {
     
                        'code': 200,
                        'message': 'success'
                    }
                    return jsonify(res)
                else:
                    # 保存用例文件
                    file_path = os.path.join(now_path, f'./case_files/{
       now}_{
       test_file.filename}')
                    test_file.save(file_path)

                    # 保存数据库
                    testcase = TestCaseModel(name=name, file_path=file_path)
                    db.session.add(testcase)
                    db.session.commit()

                    res = {
     
                        'code': 200,
                        'message': 'success'
                    }
                    return jsonify(res)
            else:
                res = {
     
                    'code': 404,
                    'message': '用例名字不能重复'
                }
                return jsonify(res)

        else:
            # 参数校验不通过
            res = {
     
                'code': 404,
                'message': form.errors
            }
            return jsonify(res)

# 查看用例视图函数
class GetTestCaseView(Resource):
    def get(self):
        form = GetTestCaseForm(request.args)
        if form.validate():
            id = form.id.data
            id_fag = db.session.query(TestCaseModel).filter(TestCaseModel.id==id).first()

            # 查看id是否在数据库中存在
            if id_fag is not None:
                res = {
     
                    'code': 200,
                    'id': id_fag.id,
                    'name': id_fag.name,
                    'description': id_fag.description,
                    'file_path': id_fag.file_path,
                    'update_time': str(id_fag.update_time)
                }
                return jsonify(res)
            else:
                res = {
     
                    'code': 404,
                    'message': '未找到对应的用例信息'
                }
                return jsonify(res)

        else:
            res = {
     
                'code':404,
                'message':form.errors
            }
            return jsonify(res)

# 用例文件下载视图函数
class DownloadTestCaseView(Resource):
    def get(self):
        form = DownloadTestCaseForm(request.args)

        if form.validate():
            # 校验通过
            id = form.id.data # 获取请求中的id
            testcase = db.session.query(TestCaseModel).filter(TestCaseModel.id == id).first() # 根据id在数据库中查询用例数据

            if testcase is not None:
                file_path = testcase.file_path
                file_name = file_path.split('/')[-1] # 获取用例文件名

                # 返回用例文件。使用send_from_directory来返回本地的文件,第一个参数为目录,第二个参数为文件名
                # 注意:真正的项目中我们不会将文件储存在本地目录下,而是储存在git或者其他储存服务器中
                file = send_from_directory(f'./case_files',filename=file_name,as_attachment=True)
                return file
            else:
                res = {
     
                    'code': 404,
                    'message': '未找到对应的用例'
                }
                return jsonify(res)

        else:
            res = {
     
                'code': 404,
                'message': form.errors
            }
            return jsonify(res)

# 用例执行视图函数
class RunTestCaseView(Resource):
    def get(self):
        form = RunTestCaseForm(request.args)
        # 判断请求参数是否合法
        if form.validate():
            id = form.id.data
            test_case = db.session.query(TestCaseModel).filter(TestCaseModel.id == id).first()
            if test_case is not None:
                id = test_case.id
                file_path = test_case.file_path
                file_name = file_path.split('/')[-1]  # 获取用例文件名

                # 调用Jenkins接口
                # 取run_testcase最后一次构建对象,根据该对象可以获取任务最后一次次构建的相关信息
                old_build = api.get_latest_build(jenkinsurl='http://192.168.1.104:8080/',
                                           jobname='run_testcase',
                                           username='xxx',
                                           password='xxxxxxxxxxxxxxxxxxxxxxxxxxxx')
                old_build_number = old_build.buildno # buildno返回构建序号

                # 执行Jenkins任务run_testcase,其中password为生成的token
                j = Jenkins("http://192.168.1.104:8080/",
                            username="ouyi",
                            password="11bafbfa9f21293159f4c5fa06f70e3ed1")
                j.build_job(jobname='run_testcase',
                            params={
     "file_name": file_name, 'id': id}) #params中的参数会传递给Jenkins中,用于任务构建

                # 将最新的构建序号更新到数据库中
                test_case.jenkins_last_build_number = old_build_number + 1 # 最新一次的构建序号
                db.session.add(test_case)
                db.session.commit()

                res = {
     
                    'code': 200,
                    'message': 'run success'
                }
                return jsonify(res)

            else:
                res = {
     
                    'code': 404,
                    'message': '未找到相应用例'
                }
                return jsonify(res)



        else:
            res = {
     
                'code': 404,
                'message': form.errors
            }
            return jsonify(res)

# 上传用例报告视图函数
class CaseReportView(Resource):
    def post(self):
        now = datetime.now().strftime('%Y-%m-%d-%H%M%S')  # 生成时间戳
        now_path = os.getcwd()  # 获取当前目录路径

        # 实例化TestCaseForm对象,用于参数校验。
        # CombinedMultiDict将两个属性组合起来传给校验器
        form = CaseReportForm(CombinedMultiDict([request.form, request.files]))
        if form.validate():
            # 如果参数校验通过

            case_id = form.case_id.data
            test_case = db.session.query(TestCaseModel).filter(TestCaseModel.id == case_id).first()

            if test_case is not None:
                report_file = form.report_file.data # 获取报告文件对象
                # 拼接成报告的保存路径,注意:正常项目中我们需要将文件保存到文件储存的服务器上,我们这里只是保存到本地
                report_file_path = os.path.join(now_path,f'report_files/{
       now}_{
       report_file.filename}')
                report_file.save(report_file_path)

                # 将报告信息保存到数据库中
                case_report = CaseReportModel(caseid=case_id,file_path=report_file_path)
                db.session.add(case_report)
                db.session.commit()
                res = {
     
                    'code': 200,
                    'message': 'success'
                }
                return jsonify(res)

            else:
                res = {
     
                    'code': 404,
                    'message': '未找到对应的用例'
                }
                return jsonify(res)

        else:
            res = {
     
                'code': 404,
                'message': form.errors
            }
            return jsonify(res)

api_.add_resource(TestCaseStoreView, '/testcase_store/', endpoint='testcase_store')
api_.add_resource(GetTestCaseView, '/get_testcase/', endpoint='get_testcase')
api_.add_resource(DownloadTestCaseView, '/download_testcase/', endpoint='download_testcase')
api_.add_resource(RunTestCaseView, '/run_testcase/', endpoint='run_testcase')
api_.add_resource(GetTestCaseStatusView, '/get_testcase_status/', endpoint='get_testcase_status')
api_.add_resource(CaseReportView, '/upload_case_report/', endpoint='upload_case_report')

if __name__ == "__main__":
    app.run(debug=True,host='127.0.0.1',port=5055)


if __name__ == "__main__":
    app.run(debug=True,host='127.0.0.1',port=5055)

1.5 在执行用例的Jenkins任务中,调用上传报告的接口

我们这个上传用例报告的接口,并不需要用户调用,而是在Jenkins任务中,执行完用例文件后,生成测试报告。并调用上传报告接口,将报告储存到服务器上。

修改Jenkins的run_testcase任务的配置:
python自动化(七)自动化测试平台开发:6.后端开发之用例报告的上传_第1张图片
我们调用一下执行用例的接口运行用例,看看是否能正常生成并上传测试报告文件。

python自动化(七)自动化测试平台开发:6.后端开发之用例报告的上传_第2张图片
看看Jenkins中的任务执行状况:
python自动化(七)自动化测试平台开发:6.后端开发之用例报告的上传_第3张图片
看看数据库中的报告表是否有数据:

在这里插入图片描述
看看用例保存路径下是否有报告文件

python自动化(七)自动化测试平台开发:6.后端开发之用例报告的上传_第4张图片
说明我们的接口是ok的。

2.用例相关操作接口联调

到这里我们已经完成了用例上传,用例下载,用例执行,用例执行状态查询,用例报告文件上传等接口。这些接口已经可以支撑我们平台的简单运行了,当然实际上企业级的测试平台比这个复杂得多。有兴趣的同学可以自己研究。

因为我们现在还没开发前端,没法在界面上操作整个流程。所以我们使用python的requests模块,来打通整个流程。

先新建一个test_baidu_case.py作为用例文件

import os
import time
import allure
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By


@allure.feature('百度搜索模块测试')
class TestBaiduSearch:
    def setup(self):
        """前置动作"""
        self.driver = webdriver.Chrome(executable_path=r'E:\project\test_baidu_search\plugin\windows\chromedriver.exe')
        self.driver.maximize_window()
        self.driver.implicitly_wait(5)

    def teardown(self):
        """后置动作"""
        self.driver.quit()


    @allure.story('百度搜索测试用例')
    @pytest.mark.parametrize("name", [("狗"), ("猫"), ("马")])
    def test_baidu_search(self, name):
        self.driver.get("https://www.baidu.com/")
        time.sleep(5)
        self.driver.find_element(By.ID, "kw").send_keys(f"{
       name}")
        self.driver.find_element(By.ID, "su").click()
        time.sleep(5)
        r = self.driver.title
        assert r == f"{
       name}_百度搜索"

编写test_api.py文件,来实现上传用例,执行用例,查询用例执行状态,上传测试报告多个接口的联调。



import time
import requests


# 上传用例
print('>>>>>>上传用例>>>>>>>')
with open(file='./test_baidu_case.py', mode='rb') as f:
    file = {
     "test_file": f}
    data = {
     "name":"test_baidu_case"}
    # 调用上传用例文件接口`/testcase_store/`
    res = requests.post('http://127.0.0.1:5055/testcase_store/',files=file,data=data)
    print(res.json())

# 运行用例
time.sleep(5)
print('>>>>>>>运行用例>>>>>>')
res = requests.get('http://127.0.0.1:5055/run_testcase/?id=20') # 调用执行用例接口
print(res.json())
time.sleep(2)


# 查询用例执行状态
print('>>>>>>查询用例执行状态>>>>>>>')
while True:
    res = requests.get('http://127.0.0.1:5055/get_testcase_status/?id=20') # 调用查询用例状态接口
    status = res.json().get('status')
    if status == 'complete': # 如果用例已执行结束则退出循环
        print(res.json())
        break
    else:
        print(res.json())
        time.sleep(5)  # 5秒调用一次查询状态接口

# 注意:用例下载接口和用例报告上传接口不需要我们单独调用,而是在执行用例的Jenkins任务中调用的。

运行结果如下:

>>>>>>上传用例>>>>>>>
{'code': 200, 'message': 'success'}
>>>>>>>运行用例>>>>>>
{'code': 200, 'message': 'run success'}
>>>>>>查询用例执行状态>>>>>>>
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': None, 'status': 'building'}
{'code': 200, 'result': 'SUCCESS', 'status': 'complete'}

python自动化(七)自动化测试平台开发:6.后端开发之用例报告的上传_第5张图片

可以看到用例执行成功了,且测试报告也上传到了服务器上。到这里我们后端接口就差不多完成了。

你可能感兴趣的:(python自动化,python,flask,软件测试)