Httprunner集成Sql数据库验证

Httprunner集成Sql验证

  • 开发功能点:
    1、sql语句可参数化
    2、sql结果支持JsonPath提取
    3、查询结果收集到httprunner Summary信息中
    4、定制场景请求前与请求后查询(如:充值接口)
  • 代码
  1. 继承Runner类,重写_run_test方法
from httprunner.runner import Runner
from httprunner import exceptions, logger, response, utils
from httprunner.validator import Validator
from utils.httpManage.db import DbMysql
from httprunner.client import HttpSession


class ARunner(Runner):
    """ Running testcases. """

    def __init__(self, config, http_client_session=None):
        """
            config =  {
                        "name": "XXXX",
                        "base_url": "http://127.0.0.1",
                        "verify": False,
                        "db_connect": {  #  db_mysql connect
                              "host": "",
                              "port": "",
                              "user": "",
                              "pwd": "",
                              "database": "",
                          }
                }
        """
        super(ARunner, self).__init__(config, http_client_session)
        self.config_db_connect = config.get("db_connect", {})  # config增加sql连接信息
        self.config_db_connect.update({"session_context": self.session_context})
        self.sql_results = {}  # sql查询结果

    def do_query_actions(self, actions, a_type):
        """ 增加sql查询操作方法

        Args:
            actions (list):
                    [{"variable_name": '', "sql": "", "json_path":""}]
            a_type (str): setup_db/teardown_db
        """
        logger.log_debug("\n==================query {} details ==================\n".format(a_type))
        with DbMysql(**self.config_db_connect) as db:
            db.do_find(actions)
            self.sql_results[a_type] = db.sql_results

    def _run_test(self, test_dict):
        """ run single teststep.

        Args:
            test_dict (dict): teststep info
                {
                    "name": "teststep description",
                    "skip": "skip this test unconditionally",
                    "times": 3,
                    "variables": [],            # optional, override
                    "request": {
                        "url": "http://127.0.0.1:5000/api/users/1000",
                        "method": "POST",
                        "headers": {
                            "Content-Type": "application/json",
                            "authorization": "$authorization",
                            "random": "$random"
                        },
                        "json": {"name": "user", "password": "123456"}
                    },
                    "extract": {},              # optional
                    "validate": [],             # optional
                    "setup_hooks": [],          # optional
                    "teardown_hooks": []        # optional
                    "setup_dbs": [
                        {"variable_name": '', "sql": "", "json_path":""}
                        ...
                    ],
                    "teardown_dbs":[
                        {"variable_name": '', "sql": "", "json_path": ""}
                        ...
                    ]
                }

        Raises:
            exceptions.ParamsError
            exceptions.ValidationFailure
            exceptions.ExtractFailure

        """
        # clear meta data first to ensure independence for each test
        self.__clear_test_data()

        # check skip
        self._handle_skip_feature(test_dict)

        # prepare
        test_dict = utils.lower_test_dict_keys(test_dict)
        test_variables = test_dict.get("variables", {})
        self.session_context.init_test_variables(test_variables)

        # teststep name
        test_name = self.session_context.eval_content(test_dict.get("name", ""))

        # parse test request
        raw_request = test_dict.get('request', {})
        parsed_test_request = self.session_context.eval_content(raw_request)
        self.session_context.update_test_variables("request", parsed_test_request)

        # prepend url with base_url unless it's already an absolute URL
        url = parsed_test_request.pop('url')
        base_url = self.session_context.eval_content(test_dict.get("base_url", ""))
        parsed_url = utils.build_url(base_url, url)

        # setup hooks
        setup_hooks = test_dict.get("setup_hooks", [])
        if setup_hooks:
            self.do_hook_actions(setup_hooks, "setup")

        # setup dbs  新增请求前sql查询
        setup_dbs = test_dict.get("setup_dbs", [])
        if setup_dbs:
            self.do_query_actions(teardown_dbs, "setup_db")
        try:
            method = parsed_test_request.pop('method')
            parsed_test_request.setdefault("verify", self.verify)
            group_name = parsed_test_request.pop("group", None)
        except KeyError:
            raise exceptions.ParamsError("URL or METHOD missed!")

        # TODO: move method validation to json schema
        valid_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
        if method.upper() not in valid_methods:
            err_msg = u"Invalid HTTP method! => {}\n".format(method)
            err_msg += "Available HTTP methods: {}".format("/".join(valid_methods))
            logger.log_error(err_msg)
            raise exceptions.ParamsError(err_msg)

        logger.log_info("{method} {url}".format(method=method, url=parsed_url))
        logger.log_debug(
            "request kwargs(raw): {kwargs}".format(kwargs=parsed_test_request))

        # request
        resp = self.http_client_session.request(
            method,
            parsed_url,
            name=(group_name or test_name),
            **parsed_test_request
        )
        resp_obj = response.ResponseObject(resp)

        # teardown hooks
        teardown_hooks = test_dict.get("teardown_hooks", [])
        if teardown_hooks:
            self.session_context.update_test_variables("response", resp_obj)
            self.do_hook_actions(teardown_hooks, "teardown")
            self.http_client_session.update_last_req_resp_record(resp_obj)

        # teardown dbs  新增请求后查询sql
        teardown_dbs = test_dict.get("teardown_dbs", [])
        if teardown_dbs:
            self.do_query_actions(teardown_dbs, "teardown_db")

        # extract
        extractors = test_dict.get("extract", {})
        extracted_variables_mapping = resp_obj.extract_response(extractors)
        self.session_context.update_session_variables(extracted_variables_mapping)

        # validate
        validators = test_dict.get("validate") or test_dict.get("validators") or []
        validate_script = test_dict.get("validate_script", [])
        if validate_script:
            validators.append({
                "type": "python_script",
                "script": validate_script
            })

        validator = Validator(self.session_context, resp_obj)
        try:
            validator.validate(validators)
        except (exceptions.ParamsError,
                exceptions.ValidationFailure, exceptions.ExtractFailure):
            err_msg = "{} DETAILED REQUEST & RESPONSE {}\n".format("*" * 32, "*" * 32)

            # log request
            err_msg += "====== request details ======\n"
            err_msg += "url: {}\n".format(parsed_url)
            err_msg += "method: {}\n".format(method)
            err_msg += "headers: {}\n".format(parsed_test_request.pop("headers", {}))
            for k, v in parsed_test_request.items():
                v = utils.omit_long_data(v)
                err_msg += "{}: {}\n".format(k, repr(v))

            err_msg += "\n"

            # log response
            err_msg += "====== response details ======\n"
            err_msg += "status_code: {}\n".format(resp_obj.status_code)
            err_msg += "headers: {}\n".format(resp_obj.headers)
            err_msg += "body: {}\n".format(repr(resp_obj.text))
            logger.log_error(err_msg)

            raise

        finally:
            # get request/response data and validate results
            self.meta_datas = getattr(self.http_client_session, "meta_data", {})
            self.meta_datas["validators"] = validator.validation_results
            self.meta_datas["sql_results"] = self.sql_results
  1. 定制sql查询 pip install pymysql
# -------------------------------------

# Author:       ALan
# Date:         2022/4/19
# Time:         13:36

# -------------------------------------

import pymysql
import jsonpath
from pymysql import OperationalError
from httprunner import logger, exceptions

class DbMysql(object):
    def __init__(self, host, port, user, pwd, database, session_context=None):
        # 建立连接
        try:
            self.conn = pymysql.connect(host=host,  # 数据库地址
                                        port=int(port),  # 端口号
                                        user=user,  # sql账号
                                        password=pwd,  # sql密码
                                        database=database,  # 数据库名
                                        charset='utf8',
                                        cursorclass=pymysql.cursors.DictCursor)
        except OperationalError as err_msg:
            logger.log_error(str(err_msg))
        else:
            # 创建游标
            self.cur = self.conn.cursor()
            self.session_context = session_context
            self.sql_results = []

    def __enter__(self):
        """查询前操作"""
        self.conn.commit()  # 查询前,提交事务
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        """执行后关闭连接操作"""
        self.cur.close()  # 关闭游标
        self.conn.close()  # 关闭连接

    def find_all_result(self, sql):
        """查询sql语句所有结果"""
        self.cur.execute(sql)  # 执行sql语句
        return self.cur.fetchall()

    def do_find(self, actions):
        """
        查询操作
        :param actions: [{"variable_name": '', "sql": "", "json_path":""}]
        :return:
        """
        self.sql_results = []

        def log_print(req_resp_dict):
            msg = "\n"
            for key, value in req_resp_dict.items():
                msg += "{:<16} : {}\n".format(key, repr(value))
            logger.log_debug(msg)

        for action in actions:
            _sql = self.__eval_sql(action.get("sql", ""))  # 替换sql语句
            json_path = action.get("json_path", "")
            sql_value = self._extract_field_with_jsonpath(self.find_all_result(_sql), json_path)  # 提取查询数据
            sql_variables_mapping = {action.get("variable_name"): sql_value}
            self.session_context.update_session_variables(sql_variables_mapping)  # 更新结果变量
            sql_dict = {
                "sql": _sql,
                "json_path": json_path,
                "sql_value": sql_value
            }
            log_print(sql_dict)
            self.sql_results.append(sql_dict)

    def __eval_sql(self, sql_item):
        """ evaluate sql item. """
        sql_value = self.session_context.eval_content(sql_item)
        return sql_value

    def _extract_field_with_jsonpath(self, data, field):
        """
        :param data: 数据源
        :param field: Jsonpath
                    expression, e.g.
                    1)$.code
                    2) $..items. *.id
        """
        if field.startswith("$"):
            result = jsonpath.jsonpath(data, field)
            if result:
                return result[0]  # 去除列表
            else:
                raise exceptions.ExtractFailure("\tjsonpath {} get nothing\n".format(field))
        else:
            return field
  • 至此已实现了基础功能点。最终是要完成Sql结果与实际数据校验,那么Validator类需要重写么?
    因原httprunner源码中已实现了Validator,可以不需要改动,仅需在api文件_add_tests方法中把Runner类改成自定义ARunner类就行。但是以上代码遗留了一个问题是在用例结构体validate中定义($参数名),必须在变量variables中定义该参数名称否则运行代码会报找不到参数错误
    例:
    image.png
  • 完整用例结构体

    image.png

  • 日志效果图

    image.png

    image.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容