测试框架助力提高编码效率

Posted by comeyke on 08-05,2023

导语

基于测试金字塔分层的理念,金字塔模型自底向上依次是单元测试、接口测试、UI测试。自动化测试的 ROI 是自底向上由高到低。测试人员进行接口自动化测试,可以获得高的ROI。
现有的接口测试工具不能完全满足软件工程迭代各个阶段的业务需求,设计一个符合产品迭代测试需求的自动化测试框架可以有效的解决工具使用上的痛点。

框架设计

进行模块包分层-功能的区分:
封装的公共方法(通用方法/事件方法)
环境开关
日志采集
测试用例管理
测试数据管理
测试报告管理
执行测试用例的入口
接口自动化测试框架搭建好后,编写一个测试用例的demo如图所示:
image-1697530873465
框架帮助我们编写更加简单而且好维护的自动化用例,让我们把主要精力放在测试用例的设计上。编码时,只需要把测试用例转化成测试数据文件,复用已封装好的方法进行接口自动化测试用例的编写即可。
一套好的测试框架,可以让团队其他同事不需要有很强的代码基础,就能编写自动化测试用例、维护测试用例、执行自动化用例,利于团队协作,起到提质保效的作用。

框架实现

image-1697530644904

业务的请求方法封装

通用的业务请求结构封装起来。如http请求方法的封装。

import requests
from common.caseLogsMethod import info
import json


class ApiRe:
    @staticmethod
    def note_post(url, user_id, sid, body, new_headers=None):
        if new_headers is not None:
            headers = new_headers
        else:
            headers = {
                'Content-Type': 'application/json',
                'X-user-key': user_id,
                'Cookie': f'wps_sid={sid}'
            }

        info(f'url:{url}')
        info(f'headers:{json.dumps(headers)}')
        info(f'body:{json.dumps(body)}')
        res = requests.post(url=url, headers=headers, json=body)
        info(f'res code:{res.status_code}')
        info(f'res body:{res.json()}')
        return res

    @staticmethod
    def note_get(url, sid):
        cookie = {
            'wps_sid': sid
        }
        info(f'url:{url}')
        info(f'cookie:{json.dumps(cookie)}')
        res = requests.get(url=url, cookies=cookie)
        info(f'res code:{res.status_code}')
        info(f'res body:{res.json()}')
        return res

日志装饰器

日志数据的采集,是测试执行的过程数据,更重要的功能是可快速定位问题,协助开发解决BUG。必要日志输出的内容有:

  • 用例名
  • 用例注释
  • 代码路径、类名、模块名
  • 事务步骤
  • 接口的请求数据和返回数据
  • 执行时间
  • 预期结果和实际结果
    对标开发的日志结构,也有info日志、error日志、step日志
    Info日志:过程步骤,接口请求数据,接口返回数据
    Error日志:捕捉到的异常行为
    step过程步骤:每个事务的开始时需要描述,比方说开始请求XX接口、造数据、删除数、sleep等待
from datetime import datetime
import inspect
import os
from colorama import Fore
from common.getProjectPath import *
import time
import functools


def info(text):
    current_time = datetime.now()
    formatted_time = current_time.strftime('%H:%M:S.%f')[:-3]  # 获取当前时间,精确到毫秒
    stack = inspect.stack()
    code_path = f"{os.path.basename(stack[1].filename)}:{stack[1].lineno}"
    text = f"[INFO]{code_path}-{formatted_time} >> {text} \n"
    print(Fore.LIGHTGREEN_EX + text.strip())
    str_time = current_time.strftime("%Y%m%d")
    log_name = "{}_info.log".format(str_time)
    with open(os.path.join(LOG_DIR, log_name), mode="a", encoding="utf-8") as f:
        f.write(text)


def error(text):
    current_time = datetime.now()
    formatted_time = current_time.strftime('%H:%M:%S.%f')[:-3]
    stack = inspect.stack()  # 获取方法执行的代码路径
    code_path = f"{os.path.basename(stack[1].filename)}:{stack[1].lineno}"
    text = f"[ERROR]{code_path}-{formatted_time} >> {text} \n"
    print(Fore.LIGHTRED_EX + text.strip())
    str_time = current_time.strftime("%Y%m%d")
    log_name = "{}_error.log".format(str_time)
    with open(os.path.join(LOG_DIR, log_name), mode="a", encoding="utf-8") as f:
        f.write(text)


def step(text):
    current_time = datetime.now()
    formatted_time = current_time.strftime('%H:%M:%S.%f')[:-3]
    stack = inspect.stack()  # 获取方法执行的代码路径
    code_path = f"{os.path.basename(stack[1].filename)}:{stack[1].lineno}"
    text = f"[INFO]{code_path}-{formatted_time} >> {text} \n"
    print(Fore.LIGHTBLUE_EX + text.strip())
    str_time = current_time.strftime("%Y%m%d")
    log_name = "{}_info.log".format(str_time)
    with open(os.path.join(LOG_DIR, log_name), mode="a", encoding="utf-8") as f:
        f.write(text)


def func_case_log(func):
    @functools.wraps(func)  # 不影响原有变量
    def inner(*args, **kwargs):
        print("")
        info(Fore.LIGHTBLUE_EX + "--------------------CASE START---------------------------")
        class_name = args[0].__class__.__name__  # 获取类名
        method_name = func.__name__  # 获取方法名
        docstring = inspect.getdoc(func)  # 获取方法注释
        info(f"Class Name: {class_name}")
        info(f"Method Name: {method_name}")
        info(f"Test Description: {docstring}")
        return func(*args, **kwargs)

    return inner


def class_case_log(cls):
    """用例的日志装饰器类级别"""
    for name, method in inspect.getmembers(cls, inspect.isfunction):
        if name.startswith('testCase'):
            setattr(cls, name, func_case_log(method))
    return cls

image-1697531786843

数据驱动yaml文件读取方法

测试数据的分层:
环境级别:主要是IP地址和端口,还有其他应用服务配置相关的信息
接口级别:API的请求方法,请求头,请求体,错误码等

import yaml
from common.getProjectPath import *


class ReadYaml:
    """读取配置数据"""

    @staticmethod
    def get_env():
        with open(os.path.join(CONF_DIR, 'env.yml'), 'r', encoding='utf-8') as f:
            data = yaml.load(f, Loader=yaml.FullLoader)
            return data['env']

    @staticmethod
    def env_yaml():
        env = ReadYaml().get_env()
        if env == 'Online':
            with open(os.path.join(ONLINE_CONF_DIR, 'config.yml'), 'r', encoding='utf-8') as f:
                return yaml.load(f, Loader=yaml.FullLoader)
        else:
            with open(os.path.join(OFFLINE_CONF_DIR, 'config.yml'), 'r', encoding='utf-8') as f:
                return yaml.load(f, Loader=yaml.FullLoader)

    @staticmethod
    def api_yaml(filename, filedir=None):
        if filedir is not None:
            path = os.path.join(DATA_DIR, filedir)
            with open(os.path.join(path, filename), 'r', encoding='utf-8') as f:
                return yaml.load(f, Loader=yaml.FullLoader)
        else:
            with open(os.path.join(DATA_DIR, filename), 'r', encoding='utf-8') as f:
                return yaml.load(f, Loader=yaml.FullLoader)

测试执行方法

依赖于测试加载器LestLoader中的pattern形参和通配符,实现用例级别的切换

import unittest
from BeautifulReport import BeautifulReport
from common.getProjectPath import *

testLoader = unittest.TestLoader()


def run(test_suite):
    # 定义输出的文件位置和名字
    filename = "report.html"
    result = BeautifulReport(test_suite)
    result.report(filename=filename, description='测试报告')


if __name__ == '__main__':
    pattern = 'all'  # all执行全量用例,smoking冒烟用例
    if pattern == 'all':
        suite = testLoader.discover("./testCase", "test*.py")
    else:
        suite = testLoader.discover("./testCase", "test_level1*.py")
    run(suite)

总结

自动化测试的本质,它是一个有业务需求的软件实现。框架的使用可以实现一份代码,多数据运行,多环境运行。提高代码的复用率的同时增强了测试代码的可靠性。