目录[-]

一、背景

在智能云科负责iSESOL BIZ项目时,遇到几个痛点:

  • 接口数量多(300+),手工回归耗时2天
  • 接口之间依赖复杂(登录token、订单号传递)
  • 业务场景多样(5种询盘单、12种订单模式)
  • 开发频繁改动,需要快速回归验证

于是设计了一套基于POM分层架构的接口自动化框架,核心目标:

分层设计:基础层、业务层、用例层分离

可复用:接口方法一次封装,到处调用

可维护:接口变更只改一处

可观测:集成Allure报告,一目了然​​​​​​​


二、技术架构

层级

模块

技术栈

职责

基础层

api_handler.py

requests + allure + logging

封装HTTP请求、装饰器、配置管理

业务层

isesol.py

继承配置类

封装具体业务接口(登录、询盘、订单等)

用例层

test_isesol.py

pytest + allure

编写测试用例、断言、报告

工具层

log_config.py

logging

日志管理、控制台+文件输出

执行层

run_tests.py

pytest + allure

批量执行、生成报告

整体目录结构:

class SimpleConfig:
    configs = {
        "isesol": {
            "url": "https://api.isesol.com/cgi/",
            "headers": {
                "Accept": "application/json, text/plain, */*",
                "Content-Type": "application/json;charset=UTF-8",
                "User-Agent": "Mozilla/5.0 ..."
            }
        }
    }

    @classmethod
    def get(cls, name):
        return cls.configs.get(name, {})

 

三、核心代码实现

1. 配置管理(SimpleConfig)

最简单的方式,把API配置存在类变量里:

def handle_api_request(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        # 获取请求数据
        params = func(self, *args, **kwargs)

        # 发送请求
        response = requests.post(
            url=params.get("url", self.url),
            headers=final_headers,
            json=params.get("data", {}),
            timeout=30
        )

        # 记录到Allure
        with allure.step(f"执行 {func.__name__}"):
            allure.attach(json.dumps(request_data, indent=2), 
                          name="请求参数")

        # 处理响应
        if hasattr(self, 'modify_resp'):
            return self.modify_resp(response)
        return response.json()
    return wrapper

2. 请求装饰器(核心)

用一个装饰器统一处理:请求日志、Allure报告、异常捕获、响应处理:

def handle_api_request(func):
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        # 获取请求数据
        params = func(self, *args, **kwargs)

        # 发送请求
        response = requests.post(
            url=params.get("url", self.url),
            headers=final_headers,
            json=params.get("data", {}),
            timeout=30
        )

        # 记录到Allure
        with allure.step(f"执行 {func.__name__}"):
            allure.attach(json.dumps(request_data, indent=2), 
                          name="请求参数")

        # 处理响应
        if hasattr(self, 'modify_resp'):
            return self.modify_resp(response)
        return response.json()
    return wrapper

3. 业务接口封装

每个接口封装成一个方法,返回值给装饰器处理:

class isesol_Main:
    def __init__(self, api_name="isesol"):
        config = SimpleConfig.get(api_name)
        self.headers = config.get("headers", {}).copy()
        self.url = config.get("url", "")

    @handle_api_request
    def getRecommendEnquiryList(self):
        """获取推荐询价列表"""
        return {
            "data": {
                "cmd": "gateway_basewebfront/enquiry/getRecommendEnquiryList"
            }
        }

    @handle_api_request
    def queryIndexBanner(self):
        """查询首页Banner"""
        return {
            "data": {
                "cmd": "gateway_basewebfront/webBanner/queryIndexBanner"
            }
        }

4. 日志配置

同时输出到控制台和文件,按日期分割:

def setup_logging(log_dir="./logs", level=logging.INFO):
    log_file = os.path.join(log_dir, f"test_{datetime.now().strftime('%Y%m%d')}.log")

    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    file_handler = logging.FileHandler(log_file, encoding='utf-8')
    console_handler = logging.StreamHandler()

    logger = logging.getLogger("api_test")
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    return logger

5. 测试用例编写

使用pytest + allure,一个接口一个测试方法:

@allure.feature("ISESOL API测试")
class TestIsesolAPI:
    def setup_class(self):
        self.api = isesol.isesol_Main()

    @allure.story("推荐询价列表接口")
    @allure.title("测试获取推荐询价列表")
    def test_get_recommend_enquiry_list(self):
        result = self.api.getRecommendEnquiryList()
        assert result is not None

6. 执行入口

批量运行、自动清理旧报告、生成Allure报告:

if __name__ == "__main__":
    # 清理旧报告
    for folder in ["./allure_results", "./html_result"]:
        if os.path.exists(folder):
            shutil.rmtree(folder)
        os.makedirs(folder, exist_ok=True)

    # 执行测试
    pytest.main([
        "test_cases/",
        "-v",
        "-s",
        "--alluredir=./allure_results"
    ])

    # 生成HTML报告
    os.system("allure generate ./allure_results -o ./html_result/ --clean --single-file")

四、踩坑记录

1:装饰器内self丢失

  • 现象:装饰器内访问不到实例的 self.headers
  • 解决:装饰器包装的是实例方法,wrapper 的第一个参数是 self,直接使用即可

2:配置被多个实例修改

  • 现象:一个实例改了headers,其他实例也受影响
  • 解决self.headers = config.get("headers", {}).copy(),复制一份

3:Allure报告中文乱码

  • 现象Allure报告里中文显示为 ???
  • 解决allure.attach(json.dumps(data, indent=2, ensure_ascii=False)) 加 ensure_ascii=False

4:requests请求被代理拦截

  • 现象:本地开了代理,请求走代理导致失败
  • 解决proxies={"http": None, "https": None} 禁用代理

5:响应格式不统一

  • 现象:有的接口返回JSON,有的返回纯文本
  • 解决:在 modify_resp 里统一处理,try: return response.json() except: return {}

五、优化方向

  1. 支持更多HTTP方法:当前只支持POST,可扩展GET、PUT、DELETE
  2. 支持动态参数{ "timestamp": "$now" } 自动替换
  3. 接口依赖管理:登录token自动传递,无需每个用例手动处理
  4. 数据驱动:测试数据放在Excel/YAML,用例层只写逻辑
  5. 支持多环境切换dev/test/prod配置分开

六、使用示例

# 安装依赖
pip install -r requirements.txt

# 运行测试
python run_tests.py

# 查看报告
allure open html_result

运行后,Allure报告会自动生成在 html_result/ 目录,打开后可以看到:

  • 每个接口的请求参数、响应数据
  • 测试步骤、日志
  • 失败原因、截图(可扩展)​​​​​​​

七、总结

这套框架的核心思想是:让测试人员只关注业务逻辑,不关心HTTP细节

  • 新人入职,只需要在 cases/ 目录下新增接口方法,在 test_cases/ 下写断言
  • 接口变更,只改 cases/ 里对应的封装,用例层不用动
  • 框架维护者(我)负责 base/ 和 run_tests.py,其他人只用干活

分层设计的价值就在这里:把复杂度关在笼子里,把简单留给使用者。