menu dzf
search self_improvement
目录
从 0 到 1 搭建接口自动化框架
dzf
dzf 2022年01月04日  ·  阅读 114

一.软件测试新挑战与自动化曙光

如今软件迭代似火箭飞驰,手工测试接口宛如小马拉大车,力不从心。自动化测试框架应运而生,成质量把关强将,开启高效测试大门,以下将详细阐述如何从无到有构建一个高效、可靠的接口自动化框架。

二.框架搭建痛点剖析

(一)编码效率瓶颈

前置后置冗余:传统编写方式下,每条用例需重复编写接口请求、日志输出及异常处理,如在多个接口测试中频繁设置请求头、处理超时异常,代码量大且易出错,拖慢开发进度。
断言成本高昂:针对接口返回数据的复杂校验,如状态码、多字段完整性、数据类型及动态值规则校验,需编写大量断言语句。不同用例中的相似断言逻辑无法复用,致使编码效率低下。
变量复用难题:用例间变量复用困难,如用户 ID、会话 ID 等环境或接口维度变量,频繁重复定义与初始化,不仅增加代码长度,还易引发变量不一致问题,影响测试稳定性。

(二)问题定位困境

日志缺失关键:缺乏日志文件存储历史运行数据,测试失败时难以追踪请求与响应详情。无法确定是接口问题、数据问题还是环境因素导致失败,排查成本极高,延长问题修复周期。
用例关联模糊:用例执行依赖关系不明,一个用例失败可能中断后续用例,且难以判断故障根源是否影响其他用例。例如,数据依赖用例顺序执行时,前置用例失败,后续用例受影响却无法快速定位关联。

(三)可视化与复用短板

结果输出局限:控制台输出简单,难以直观呈现多条用例执行结果。测试报告功能欠缺,无法清晰展示用例状态、执行时间、断言详情等关键信息,不满足团队对测试结果量化分析与问题汇总需求。
参数化支持弱:缺少多条用例参数化功能,处理接口参数变化场景需大量复制粘贴修改用例代码。如测试不同用户权限下接口功能,为每个权限编写独立用例,代码重复度高、维护困难。

(四)环境与数据管理混乱

环境切换复杂:不同环境(测试、预发布、线上)配置切换繁琐,手动修改代码或配置文件易出错,增加测试环境部署成本与出错风险,降低测试效率。
数据驱动缺失:测试数据与代码紧密耦合,数据变更需改动代码,缺乏数据驱动思想。如接口参数随业务调整,需在众多用例中逐个修改对应数据,可维护性差。
用例独立性差:用例依赖被测试数据状态,未实现数据隔离。如删除便签用例执行后,后续获取便签用例因数据已删而失败,无法保证用例独立性与可重复

三.针对性解决方案

(一)高效编码架构

框架分层封装:构建通用方法层,涵盖断言、日志、配置读取与数据库交互方法;业务层封装接口请求、数据构建与清理逻辑;用例层专注测试场景编写,遵循分层架构实现高内聚低耦合。如将通用断言方法封装后供所有用例调用,减少重复编码。
变量集中管理:提取环境与接口维度变量至配置文件(YAML),用例层统一读取。如在配置文件中设置用户 ID、接口地址等变量,用例执行时按需获取,确保变量一致性与复用性。
屏幕截图 2024-12-10 151244
用例层的实例代码如下:

 def testCase01_major(self):
        """新增分组接口,主流程:用户新增分组"""
        info('用户A请求新增分组接口')
        group_id = str(int(time.time() * 1000))
        body = {
            "groupId": group_id,
            "groupName": 'test',
            "order": 0
        }
        res = self.re.post(self.url, sid=self.sid1, user_id=self.user_id1, body=body)
        expect = {
            'responseTime': int,
            'updateTime': int
        }
        self.assertEqual(200, res.status_code, msg='状态码校验失败')
        self.ga.http_assert(expect, res.json())

        info('请求获取用户分组列表信息,进行数据源的校验')

        get_url = self.host + '/v3/notesvr/get/notegroup'
        body = {'excludeInValid': True}
        res = self.re.post(url=get_url, sid=self.sid1, user_id=self.user_id1, body=body)
        self.assertTrue(len(res.json()['noteGroups']) == 1)
        self.assertTrue(res.json()['noteGroups'][0]['groupId'] == group_id)

上面代码涉及到接口请求的封装、数据的构建,断言以及日志的封装,使代码看起来很精简。
断言的封装使得我们调用只需在测试用例用一行语句描述即可,无论我们expect的结果是什么,比如包含返回值的类型校验,抑或精确值校验,还是层层嵌套的列表或字典,我们都可以通过封装好的断言一行实现。通过递归的思想设计断言逻辑,无论是多少层的字典或列表嵌套,都可以轻松实现。断言的代码demo如下:

import unittest

class GeneralAssert(unittest.TestCase):
    def http_assert(self,expected,actual):
        if isinstance(expected,dict):
            if expected == actual:
                return
            self.assertEqual(len(expected.keys()),len(actual.keys()),msg=f"{list(expected.keys())}不符,实际的key是{list(actual.keys())}")
            for key,value in expected.items():
                if isinstance(value,type):
                    self.assertEqual(value,type(actual[key]),msg=f'{key}的类型不符,实际的类型是{type(actual[key])}')
                elif isinstance(value,dict):
                    self.http_assert(value,actual[key])
                elif isinstance(value,list):
                    for i in range(len(expected[key])):
                        if isinstance(expected[key][i],type):
                            self.assertEqual(expected[key][i],type(actual[key][i]),msg=f'{expected[key]}类型不符,实际的类型是{actual[key][i]}')
                        if isinstance(expected[key][i],(dict,list)):
                            self.http_assert(expected[key][i],actual[key][i])
                        else:
                            self.assertEqual(expected[key][i],actual[key][i],msg=f'{expected[key]}的值不同,实际的值是{actual[key][i]}')
                else:
                    self.assertEqual(value,actual[key],msg=f'期望{key}值{value}不同,实际的值是{actual[key]}')
        if isinstance(expected,list):
            self.assertEqual(len(expected),len(actual),msg=f'与{list(expected)}字段个数不一致,实际的字段为{list(actual)}')
            for i in range(len(expected)):
                if isinstance(expected[i],(dict,list)):
                    self.http_assert(expected[i],actual[i])
                else:
                    self.assertEqual(expected[i],actual[i],msg=f'{expected[i]}的值错误,实际值为{actual[i]}')

接口请求封装基于request库,包含了请求异常的处理,默认请求头的封装,相关日志的输出,在定位问题时可以看到请求的详细信息,代码如下:

class BusinessRe:
    @staticmethod
    def post(url, sid, user_id, body, headers=None):
        if headers is None:
            headers = {
                'Cookie': f'sid={sid}',
                'X-user-key': str(user_id),
                'Content-Type': 'application/json'
            }

        info(f'request url: {url}')
        info(f'request headers: {headers}')
        info(f'request body: {body}')
        try:
            res = requests.post(url, headers=headers, json=body, timeout=5)
        except TimeoutError:
            error(f'url: {url}, requests timeout!')
            return 'Requests Timeout!'
        info(f'response code: {res.status_code}')
        info(f'response body: {res.text}')
        return res

数据清理是保障用例独立性的关键,下面是一个清理数据的伪代码,一般在用例的前置清空,利用框架自带的setUp函数,会在用例执行前自动调用,该代码示例了一个通过接口删除数据的过程,当然,也可以直接操作数据库删除数据,得写个数据库连接池,编写相关的sql删除,这里就不例举了。

class DataClear:
    """创建用例前置和后置数据"""
    host = 'http://note-api.wps.cn'

    def del_notes(self, user_id, sid):
        """删除用户便签"""
        note_ids = []

        # step1 获取首页便签,提取noteId
        response = requests.get(f"{self.host}/notes/home", headers={"sid": sid})
        if response.status_code == 200:
            note_ids.extend([note['id'] for note in response.json().get("notes", [])])

        # step2 获取日历便签,提取noteId
        response = requests.get(f"{self.host}/notes/calendar", headers={"sid": sid})
        if response.status_code == 200:
            note_ids.extend([note['id'] for note in response.json().get("notes", [])])

        # step3 获取分组便签,提取noteId
        response = requests.get(f"{self.host}/notes/groups", headers={"sid": sid})
        if response.status_code == 200:
            for group in response.json().get("groups", []):
                group_notes = group.get("notes", [])
                note_ids.extend([note['id'] for note in group_notes])

        # step4 循环noteId,尽量循环删除
        for note_id in note_ids:
            requests.delete(f"{self.host}/note/{note_id}", headers={"sid": sid})
            time.sleep(0.1)  # 添加短暂延迟,防止请求过快

        # step5 清空回收站
        requests.delete(f"{self.host}/notes/trash", headers={"sid": sid})

testcase.py:

from business.dataCreate import DataCreate

@class_case_decoration
class GroupCreateMajor(unittest.TestCase):
    def setUp(self) -> None:
        # 用户数据清理
        DataCreate()

(二)精准问题定位

完善日志体系:设计日志装饰器与输出方法,自动记录用例执行各阶段信息,包括请求发送、响应接收、断言结果、异常抛出等。日志按时间、文件、代码行号精准分类存储,便于快速检索排查。
用例解耦设计:从用户对象控制数据生命周期,用例初始化清理数据,前置步骤重建所需数据,后置清理残留数据。如测试用户便签功能系列用例,每个用例执行前为用户初始化全新便签数据,确保独立性。
日志是有级别的,分为info、warning、debug、error,根据需要加到相应的代码位置,即便现在有相关日志包,但是为了更加灵活的输出我们想要的接口信息,还是自己封装一个比较好,并且输出到相应的文件夹里方便之后查阅,下面代码是info日志的demo:

def info(text):
    """
    打印用例运行时数据并输出对应的日志
    :param text: str 控制台要输出的内容或要打印的日志文本数据
    :return:
    """
    formatted_time = datetime.now().strftime('%H:%M:%S:%f')[:-3]  # 定义了日志的输出时间
    stack = inspect.stack()
    code_path = f"{os.path.basename(stack[1].filename)}:{stack[1].lineno}"  # 当前执行文件的绝对路径和执行代码行号
    content = f"[INFO]{formatted_time}-{code_path} >> {text}"
    print(Fore.WHITE + content)
    str_time = datetime.now().strftime("%Y%m%d")
    with open(file=DIR + '\\logs\\' + f'{str_time}_info.log', mode='a', encoding='utf-8') as f:
        f.write(content + '\n')

(三)优化可视化与复用

强大报告生成:集成 BeautifulReport 等工具生成 HTML 测试报告,展示用例详情(名称、描述、执行时间)、断言失败信息、请求响应数据等内容,实现可视化图表分析,满足团队不同层次结果查看需求。
灵活参数化实现:基于 ddt 或 parameterized 库实现用例参数化,以列表或元组形式传入多组参数,用例自动循环执行。如为测试不同内容的便签创建用例,将便签标题、正文等参数化,一条用例覆盖多种情况。比如mustKeys就是参数,mustKey包含groupId,groupName两个参数,通过key接收,从而在body中先剔除掉第一个参数,再剔除掉下一个参数,直到mustKey中的参数遍历完结束,从而实现用例的参数化。

@parameterized.expand(mustKeys)
    def testCase01_input_must_key_remove(self, key):
        """新增分组接口,必填项缺失"""
        info('用户A请求新增分组接口')
        group_id = str(int(time.time() * 1000))
        body = {
            "groupId": group_id,
            "groupName": 'test',
            "order": 0
        }
        body.pop(key)
        res = self.re.post(self.url, sid=self.sid1, user_id=self.user_id1, body=body)
        self.assertEqual(500, res.status_code, msg='状态码校验失败')

(四)智能环境与数据管理

便捷环境切换:在 main 函数或配置读取模块定义环境常量,依据常量自动加载对应环境配置文件(YAML)。一键切换测试环境,确保配置准确性与一致性。
数据驱动转型:将测试数据存于外部文件(CSV、Excel、YAML),用例执行时读取解析。如接口参数随业务规则频繁变更,只需修改外部数据文件,用例代码零改动,提升数据维护性与用例复用性。
yaml.py:

from main import DIR, ENVIRON
import yaml


class YamlRead:
    @staticmethod
    def env_config():
        """环境变量的读取方式"""
        with open(file=f'{DIR}/data/env_config/{ENVIRON}/config.yml', mode='r', encoding='utf-8') as f:
            return yaml.load(f, Loader=yaml.FullLoader)

    @staticmethod
    def api_config():
        with open(file=f'{DIR}/data/api_config/api.yml', mode='r', encoding='utf-8') as f:
            return yaml.load(f, Loader=yaml.FullLoader)

api.yml

note_create_info:
  path: /v3/ccccccccc
  method: post
  mustKeys:
    - noteId
  notMustKeys:
    - star
    - remindTime
    - remindType
    - groupId
group_create:
  path: /v3/xxxxxxxxx
  method: post
  mustKeys:
    - groupId
    - groupName
  notMustKeys:
    - order

config.yml

user_id1: xxxxxx
sid1: 'xxxxxxxxxxxxxxxxxxxxxxxxxx'
host: 'http://xxxxxxxxxxxxxxx'

main.py:

import unittest
import os
from common.BeautifulReport import BeautifulReport

DIR = os.path.dirname(os.path.abspath(__file__))
ENVIRON = 'Online'  # 'Online' -> 线上环境, 'Offline' -> 测试环境

if __name__ == '__main__':
    run_pattern = 'all'  # all 全量测试用例执行 /  smoking 冒烟测试执行  /  指定执行文件
    if run_pattern == 'all':
        run_pattern = 'test_*.py'
    elif run_pattern == 'smoking':
        run_pattern = 'test_major*.py'
    else:
        run_pattern = run_pattern + '.py'
    suite = unittest.TestLoader().discover('./testCase', pattern=pattern)

    result = BeautifulReport(suite)
    result.report(filename="report.html", description='测试报告', report_dir='./')

封装好的读取yaml文件工具类,加载相应路径下的api.yml文件或者config文件,通过main调用,从而实现环境的切换和相关用例的参数化。

结语:回顾与展望

至此,我们已完整遍历从 0 到 1 搭建接口自动化框架的全过程。在这个过程中,我们针对编码效率、问题定位、可视化、数据管理等诸多痛点,逐一给出了切实可行的解决方案,并结合实际案例进行了深入剖析。
回顾过往,接口自动化框架从无到有的搭建历程充满挑战,但每一个问题的解决都是一次成长。它不仅提升了测试效率,更保障了软件质量,让我们在软件开发的道路上更加自信从容。
展望未来,技术在不断演进,接口自动化测试也将迎来新的机遇与挑战。我们将持续关注行业动态,不断优化框架,融入新的技术元素,如人工智能驱动的智能测试、更高效的分布式测试架构等。同时,我们也期待与更多同行交流合作,共同推动接口自动化测试领域的发展,为软件行业的蓬勃发展贡献更多力量。愿每一位踏上接口自动化测试之旅的伙伴,都能在这个充满创新与挑战的领域收获满满,携手共创软件测试的美好未来!

分类:
标签: