hxz
发布于 2023-12-10 / 128 阅读
0

【自动化桩】真实模拟三方服务

背景

对于开发人员来说,在实际项目中写单元测试的过程中会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。

了解决这类问题开发人员引入了Mock的概念。

而对于测试的我们来说,我们在测试被测服务过程中,会遇到被测服务需要依赖三方服务才能正常启动和处理业务的场景,仅仅使用mock工具没办法覆盖全部测试用例。

所以,我们可以要求开发实现一个简易版本的三方服务,确保测试用例的执行,开发可能有以下三种处理方案:

  1. 写一个桩(假想的服务),一个空心服务,只能保证通信过程,固定返回200的处理结果

  2. 修改被测服务源码,屏蔽掉所有三方服务的交互代码

  3. 写死三方服务的处理结果,不进行通信

  4. 只提供线上环境来测试

痛点

基于上述这些手段可能存在的问题和痛点:

  1. 三方服务异常的处理行为覆盖不到

  2. 定义的三方处理数据缺少真实性

  3. 没法保障被测服务上线以后能和三方服务正常通信,还需要额外的线上调试成本

  4. 开发不愿花更多的人力成本时间成本来保障可测试性

  5. 线上测试的成本过高,或可能影响线上的真实数据

价值

基于上述问题背景,引入了自动化桩的概念,在桩的基础上我实现了更高阶的能力,能够实现不仅仅只是对请求做出响应,它还能融合嵌入到接口自动化框架中,不再需要测试人员手动去执行,只需前置在用例层代码定义好三方服务的返回即可。

自动化桩实现的能力如下:

  • 能够接受桩的消息进行校验,相当于验证被测服务请求三方服务协议的正确性!

  • 能在测试用例层直接定义桩的结果返回!

  • 且全程无需开发协助实现,无需开发协助部署。

设计交互图

交互流程

  1. 首先修改被测服务的配置,指向依赖服务的地址修改为自动化桩的ip和端口

  2. 启动python与桩的通信信道,实现socket服务端

  3. 启动socket客户端

  4. 启动自动化桩的flask桩实例

  5. 启动被测服务

  6. python客户端发送请求给被测服务

  7. 被测服务发送请求给桩

  8. 桩接收被测服务的请求后发送给python客户端去断言协议正确性

  9. python客户端定义桩的返回体发送给桩

  10. 桩发送返回体给被测服务

  11. 被测服务收到桩的响应后,发送响应给python客户端

  12. python客户端断言被测服务返回体

实战演练

完整项目背景:

域管平台由人员管理、终端管理、策略中心、运维服务、软件管理、企业服务、日志审计、报表管理、消息中心、平台管理等模块组成。管理员可以通过域管平台,对终端设备进行统一用户认证管理、安全策略集中控制、软件更新与分发等功能。

问题背景:

在运维服务-任务管理模块,下发任务到设备流程中,有一个痛点问题:任务下发到关机设备时,会出现任务无法送达设备并正确执行的问题。所以,提出检测机器是否在线的方案。管理员下发任务到设备时,只有检测到设备在线,才允许下发任务。

此处,我用自动化桩模拟的三方服务就是machineServer

展开服务端的测试工作,需要设计依赖服务异常的测试用例。

请求task接口,machine返回403的状态码 期望值:task接口返回 500状态码,返回体{XXXXXX}

请求task接口,machine返回超时 期望值:task接口返回 500状态码,返回体{XXXXXX}

请求task接口,machine异常下线 期望值:task接口返回 503状态码,返回体{XXXXXX}

代码实现细节

main.py

import time
import unittest
import os
from BeautifulReport import BeautifulReport
from threading import Thread
from client_stub import machineStub
import os
import psutil

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

if __name__ == '__main__':
    # 杀python进程
    current_process = os.getpid()
    for process in psutil.process_iter():
        try:
            if "python" in process.name().lower() and process.pid != current_process:
                process.terminate()
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass

    # 启动桩
    machineStub.stub_start()

    def start_server():
        os.chdir(DIR + '/app')
        os.system('python taskServer.py')  # ./XXX.sh


    t = Thread(target=start_server)
    t.start()
    time.sleep(5)

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

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

    machineStub.shutdown_stub()

用例层

import time
import unittest
import requests
import json
from client_stub import machineStub
from httpStubFramework.httpCommon import HttpCommon
from common.logsMethod import step
from common.logsMethod import class_case_log
from common.checkOutput import CheckOutput


@class_case_log
class Pro(unittest.TestCase):
    def setUp(self) -> None:
        url = 'http://127.0.0.1:8530/clear'
        headers = {
            'Content-Type': 'application/json'
        }
        requests.delete(url=url, headers=headers, json={'machine_id': '1111'})

    def testCase01_major(self):
        """请求task接口,machine返回200的状态码 期望值:task接口返回 200状态码,返回体{XXXXXX}"""
        url = 'http://127.0.0.1:8530/publish'
        headers = {
            'Content-Type': 'application/json',
            'Cookie': 'user_id=B'
        }
        data = {
            "machine_id": "1111",
            "status": "publish"
        }
        step("实例化桩Flask服务")
        hc = HttpCommon()
        hc.http_requests(url=url, method='post', headers=headers, json=data)
        # 桩的socket接收消息
        machine_recv = machineStub.receive()
        print("machine_recv:")
        print(machine_recv)
        step("校验被测服务请求stub的协议正确性")
        self.assertEqual(machine_recv['headers'], {'Host': '127.0.0.1:8771', 'User-Agent': 'python-requests/2.27.1',
                                                'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*',
                                                'Connection': 'keep-alive'})
        self.assertEqual(machine_recv['body'], {"machine_id": "1111"})
        self.assertEqual(machine_recv['method'], "GET")

        step("用例层自定义桩的异常情况和信息")
        major_data = {
            "body": {"msg": "publish success"},
            "code": 200
        }
        step("桩收到异常信息后,发送异常信息给被测服务")
        # 桩的socket发送消息
        machineStub.send(major_data)
        time.sleep(1)
        step("用例层预期值校验:被测服务发送用例层的预期结果是200")
        expect = {"msg": "publish success"}
        hc.res_json = json.loads(hc.res_text)
        CheckOutput().output_check(expect, hc.res_json)
        self.assertEqual(200, hc.status_code)

    def testCase02_machineApp_res_403(self):
        """请求task接口,machine返回403的状态码 期望值:task接口返回 500状态码,返回体{XXXXXX}"""
        url = 'http://127.0.0.1:8530/publish'
        headers = {
            'Content-Type': 'application/json',
            'Cookie': 'user_id=B'
        }
        data = {
            "machine_id": "1111",
            "status": "publish"
        }
        step("实例化桩Flask服务")
        hc = HttpCommon()
        hc.http_requests(url=url, method='post', headers=headers, json=data)
        # 桩的socket接收消息
        machine_recv = machineStub.receive()
        step("校验被测服务请求stub的协议正确性")
        self.assertEqual(machine_recv['headers'], {'Host': '127.0.0.1:8771', 'User-Agent': 'python-requests/2.27.1',
                                                'Accept-Encoding': 'gzip, deflate', 'Accept': '*/*',
                                                'Connection': 'keep-alive'})
        self.assertEqual(machine_recv['body'], {"machine_id": "1111"})
        self.assertEqual(machine_recv['method'], "GET")
        step("用例层自定义桩的异常情况和信息")
        fail_data = {
            "body": {"msg": "fail", "body_code": 999999},
            "code": 403
        }
        step("桩收到异常信息后,发送异常信息给被测服务")
        # 桩的socket发送消息
        machineStub.send(fail_data)
        time.sleep(1)
        step("用例层预期值校验:被测服务发送用例层的预期结果是500")
        expect = {"msg": "SERVER ERROR"}
        hc.res_json = json.loads(hc.res_text)
        CheckOutput().output_check(expect, hc.res_json)
        self.assertEqual(500, hc.status_code)