中介軟體自動化測試框架 cmdlinker

阿木木發表於2025-01-03

背景

作為一箇中介軟體的測試工程師,如何對於中介軟體提供的命令進行自動化的迴歸,這一直是一個難題,市面上好像缺乏了對於命令進行自動化迴歸的合理解決方案。
常見方式有下面兩種:

  • 直接寫字串的命令,然後使用各種程式語言的 SSH 庫進行連線,然後執行命令字串,獲取執行結果,如果需要對傳入的引數進行校驗,還需要各種提取字串,或者設定各種命令變數,比較繁瑣
  • 編寫命令物件,透過操作命令物件 set/get 方式,生成命令字串給 SSH 庫進行命令執行,如果需要對傳入的引數進行校驗,只需要透過命令物件 get 的方式進行獲取,當然缺點就是,每次都需要進行大量的工作去編寫命令物件,重複性勞動

透過對比之後,會發現,使用第二種方式更加直觀、簡潔,提取響應資料也更加方便,但是第二種方式對於大量重複性的去編寫命令物件的方式,也讓人十分噁心。如果有一個工具能夠幫助大家去生成命令物件,然後透過呼叫命令物件執行請求響應的方式就能讓大家從這種困境中走出來。

所以引入了 cmdlinker 這個開源 python 庫,cmdlinker 能夠幫助我們做什麼?

  • 透過編寫命令物件 yaml 的方式,生成命令物件,無需手動進行命令物件程式碼的編寫
  • 命令物件自帶 runner,可以直接獲取命令執行的請求響應
  • 支援本地 shell 執行及遠端 ssh 執行的方式 # 安裝 ~~~shell pip3 install cmdlinker ~~~

文件地址

https://github.com/chineseluo/cmdlinker

案例演示:

使用 docker 命令舉例,查詢容器,獲取容器配置資訊,斷言容器的 ID 是否包含請求的 ID
編寫 yaml 檔案:

entry: "docker"
mapping_entry: "Docker"
module_name: "docker"
class_name: "Docker"
out_path: "./"
mode: "SSH"
parameters:
  - mapping_name: "ps"
    original_cmd: "ps"
    value: False # 是否需要值
    mutex: False # 是否互斥
    default: None
  - mapping_name: "inspect"
    original_cmd: "inspect"
    value: True # 是否需要值
    mutex: False # 是否互斥
    default: None
  - mapping_name: "f"
    original_cmd: "-f"
    value: True # 是否需要值
    mutex: False # 是否互斥
    default: None

執行生成命令:

CmdLinker init -f ./docker.yaml

生成 python 命令物件

from loguru import logger
from typing import Union, Text
from cmdlinker.builtin.exception import CmdLinkerMutexException, CmdLinkerMulMutexException
from cmdlinker.builtin.ssh_utils import SSHClient
from cmdlinker.builtin.shell_utils import ShellClient


class Cmds:
    def __init__(self):
        self.index = 0
        self.CMD_LIST = []


class Runner:

    def __init__(self):
        self._mutexs = []
        self._gcs = []
        self._not_mutexs = []
        self.pre: Runner = object
        self.root: Runner = object
        self.next: Union[Runner] = object
        self.main_cmd: Text = None
        self.cmds: Cmds = None
        self.ssh_client: SSHClient = None
        self.sudo: bool = True
        self.timeout: int = 20

    def _exclude(self, cmd_obj):
        if not cmd_obj.mark:
            logger.debug(f"命令物件:{cmd_obj.__class__.__name__}未被使用")
            return False
        else:
            return True

    def _get_execute_cmd(self, cmd_obj):
        if cmd_obj.need_value:
            return f"{cmd_obj.meta_data['original_cmd']} {cmd_obj.value}"
        else:
            return f"{cmd_obj.meta_data['original_cmd']}"

    def _get_log_desc(self):
        if self.pre == self.root:
            log_desc = "子"
        elif self.pre is None and self.root == self:
            log_desc = "根"
        else:
            log_desc = "父"
        return log_desc

    def cmd_checker(self):
        log_desc = self._get_log_desc()
        logger.info("==" * 20 + f"開啟{log_desc}命令{self.__class__.__name__}合法性檢查" + "==" * 20)
        for cmd_obj_str in vars(self).keys():
            cmd_obj = getattr(self, cmd_obj_str)
            if isinstance(cmd_obj, BaseCmd):
                if cmd_obj_str == "next":
                    continue
                if not self._exclude(cmd_obj):
                    continue
                if cmd_obj.mutex:
                    self._mutexs.append(cmd_obj)
                    logger.debug(f"{self.main_cmd}新增互斥物件:{cmd_obj},最新互斥物件列表:")
                else:
                    self._not_mutexs.append(cmd_obj)
                    logger.debug(f"{self.main_cmd}新增非互斥物件:{cmd_obj},最新非互斥物件列表:{self._not_mutexs}")
        if len(self._mutexs) > 1:
            raise CmdLinkerMulMutexException(self.__class__.__name__, self._mutexs)
        if len(self._mutexs) != 0 and len(self._not_mutexs) != 0:
            raise CmdLinkerMutexException(self.__class__.__name__, self._mutexs, self._not_mutexs)
        if self.pre == self.root:
            logger.debug(f"啟用命令物件{self.__class__.__name__} 根命令: {self.root.__class__.__name__} 檢查")
            self.pre.cmd_checker()
        elif self.pre is None and self.root == self:
            pass
        else:
            logger.debug(f"啟用命令物件{self.__class__.__name__}父命令: {self.pre.__class__.__name__} 檢查")
            self.pre.cmd_checker()
        self._not_mutexs = []
        self._mutexs = []
        logger.info("==" * 20 + f"{log_desc}命令{self.__class__.__name__}合法性檢查透過" + "==" * 20)

    def runner(self):
        self.cmd_checker()
        cmd = self.exec_cmd()
        self.cmds.CMD_LIST = []
        return self.ssh_client.run_cmd(cmd, timeout=self.timeout)

    def collector(self):
        return self.cmds.CMD_LIST.sort(key=lambda cmd_obj: cmd_obj.index)

    def exec_cmd(self):
        self.cmds.CMD_LIST.sort(key=lambda cmd_obj: cmd_obj.index)
        cmd_list = [self.main_cmd] + [self._get_execute_cmd(cmd) for cmd in self.cmds.CMD_LIST]
        if self.sudo:
            cmd_list.insert(0, "sudo")
        logger.info(f"執行命令列表:{cmd_list}")
        cmd = " ".join(cmd_list)
        return cmd


class BaseCmd(Runner):
    def __init__(self):
        super().__init__()
        self.cmds: Cmds = None
        self.meta_data = None
        self.mutex = None
        self.need_value = None
        self.has_child_cmd = None
        self.gc = None
        self.child_cmd = None
        self.default_value = None
        self.value = None
        self.mark = None
        self.index = None
        self.level = 0
        self._mutexs = []
        self._gcs = []
        self._not_mutexs = []
        self.main_cmd = "docker"
        self.pre: object = object
        self.root: object = object
        self.next: Union[object] = object
        self.ssh_client: SSHClient = None


class Ps(BaseCmd):
    def __init__(self, root_obj, pre_obj):
        super().__init__()
        self.pre: Docker = pre_obj
        self.root: Docker = root_obj
        self.next: Union[Ps] = self
        self.meta_data = {'mapping_name': 'ps', 'original_cmd': 'ps', 'value': False, 'mutex': False, 'default': 'None',
                          'has_child_cmd': False, 'child_cmds': [], 'parent_cmd': 'Docker', 'root_cmd': 'Docker'}
        self.level = 2
        self.mutex = False
        self.need_value = False
        self.has_child_cmd = False
        self.child_cmds = []
        self.gc = False

        self.default_value = "None"
        self.value = self.default_value
        self.mark = False


class Inspect(BaseCmd):
    def __init__(self, root_obj, pre_obj):
        super().__init__()
        self.pre: Docker = pre_obj
        self.root: Docker = root_obj
        self.next: Union[Inspect] = self
        self.meta_data = {'mapping_name': 'inspect', 'original_cmd': 'inspect', 'value': True, 'mutex': False,
                          'default': 'None', 'has_child_cmd': False, 'child_cmds': [], 'parent_cmd': 'Docker',
                          'root_cmd': 'Docker'}
        self.level = 2
        self.mutex = False
        self.need_value = True
        self.has_child_cmd = False
        self.child_cmds = []
        self.gc = False

        self.default_value = "None"
        self.value = self.default_value
        self.mark = False


class Format(BaseCmd):
    def __init__(self, root_obj, pre_obj):
        super().__init__()
        self.pre: Docker = pre_obj
        self.root: Docker = root_obj
        self.next: Union[Format] = self
        self.meta_data = {'mapping_name': 'format', 'original_cmd': '--format', 'value': True, 'mutex': False,
                          'default': 'None', 'has_child_cmd': False, 'child_cmds': [], 'parent_cmd': 'Docker',
                          'root_cmd': 'Docker'}
        self.level = 2
        self.mutex = False
        self.need_value = True
        self.has_child_cmd = False
        self.child_cmds = []
        self.gc = False

        self.default_value = "None"
        self.value = self.default_value
        self.mark = False


class Docker(Runner):
    def __init__(self, host=None, name=None, pwd=None, port="22", timeout="60", sudo=False):
        super().__init__()
        self.cmds: Cmds = Cmds()
        self.pre: object = None
        self.root: Docker = self
        self.next: Union[Ps, Inspect, Format,] = None
        self.main_cmd = "docker"
        self._mutexs = []
        self._gcs = []
        self._not_mutexs = []
        self.mode = "SSH"
        self.ssh_client = SSHClient(host, name, pwd, port)
        self._ps: Ps = Ps(self, self)
        self._inspect: Inspect = Inspect(self, self)
        self._format: Format = Format(self, self)

    def _set_ps(self):
        self._ps.mark = True
        self._ps.index = self.cmds.index
        self.cmds.index += 1
        self.next = self._ps
        self.cmds.CMD_LIST.append(self._ps)
        self._ps.cmds = self.cmds
        self._ps.ssh_client = self.ssh_client

    def tset_ps(self):
        """
        傳遞TRANSMIT模式,可以獲取子命令物件,可透過,root/pre/next,控制命令物件的根/父/子級
        """
        self._set_ps()
        return self._ps

    def hset_ps(self, ):
        """
        保持HOLD模式,該方法返回該物件本身,不返回子物件
        """
        self._set_ps()
        return self

    def _set_inspect(self, value=None):
        self._inspect.mark = True
        self._inspect.index = self.cmds.index
        self.cmds.index += 1
        self.next = self._inspect
        self.cmds.CMD_LIST.append(self._inspect)
        self._inspect.cmds = self.cmds
        self._inspect.ssh_client = self.ssh_client

        if self._inspect.need_value:
            self._inspect.value = value

    def tset_inspect(self, value=None):
        """
        傳遞TRANSMIT模式,可以獲取子命令物件,可透過,root/pre/next,控制命令物件的根/父/子級
        """
        self._set_inspect(value=value)
        return self._inspect

    def hset_inspect(self, value=None):
        """
        保持HOLD模式,該方法返回該物件本身,不返回子物件
        """
        self._set_inspect(value=value)
        return self

    def _set_format(self, value=None):
        self._format.mark = True
        self._format.index = self.cmds.index
        self.cmds.index += 1
        self.next = self._format
        self.cmds.CMD_LIST.append(self._format)
        self._format.cmds = self.cmds
        self._format.ssh_client = self.ssh_client

        if self._format.need_value:
            self._format.value = value

    def tset_format(self, value=None):
        """
        傳遞TRANSMIT模式,可以獲取子命令物件,可透過,root/pre/next,控制命令物件的根/父/子級
        """
        self._set_format(value=value)
        return self._format

    def hset_format(self, value=None):
        """
        保持HOLD模式,該方法返回該物件本身,不返回子物件
        """
        self._set_format(value=value)
        return self

    def ps(self) -> Ps:
        return self._ps

    def inspect(self) -> Inspect:
        return self._inspect

    def format(self) -> Format:
        return self._format

執行測試

進行請求響應斷言(PS:自行替換下面的 host/name/pwd/port 等引數驗證)

if __name__ == '__main__':
    docker = Docker(host="192.168.1.2", name="root", pwd="xxxx")
    ps_res = docker.hset_ps().hset_format("{{.ID}}").runner()
    inspect_res = docker.hset_inspect(ps_res["stdout"]).runner()
    assert ps_res["stdout"].strip() in inspect_res["stdout"][0]["Id"]

檢查日誌

C:\Users\Dell\AppData\Local\Programs\Python\Python39\python.exe E:\開源專案\CmdLinker\tests\docker.py 
2025-01-03 16:38:03.116 | INFO     | __main__:cmd_checker:53 - ========================================開啟根命令Docker合法性檢查========================================
2025-01-03 16:38:03.116 | DEBUG    | __main__:cmd_checker:66 - docker新增非互斥物件:<__main__.Ps object at 0x000001ADA171E9A0>,最新非互斥物件列表:[<__main__.Ps object at 0x000001ADA171E9A0>]
2025-01-03 16:38:03.116 | DEBUG    | __main__:_exclude:31 - 命令物件:Inspect未被使用
2025-01-03 16:38:03.116 | DEBUG    | __main__:cmd_checker:66 - docker新增非互斥物件:<__main__.Format object at 0x000001ADA171EAF0>,最新非互斥物件列表:[<__main__.Ps object at 0x000001ADA171E9A0>, <__main__.Format object at 0x000001ADA171EAF0>]
2025-01-03 16:38:03.116 | INFO     | __main__:cmd_checker:81 - ========================================根命令Docker合法性檢查透過========================================
2025-01-03 16:38:03.116 | INFO     | __main__:exec_cmd:97 - 執行命令列表:['sudo', 'docker', 'ps', '--format {{.ID}}']
2025-01-03 16:38:03.693 | INFO     | cmdlinker.builtin.logger_operation:console_output:18 - 
================== cmd request  details ==================
execute cmd on host : 192.168.1.2
execute cmd : sudo docker ps --format {{.ID}}

2025-01-03 16:38:03.694 | WARNING  | cmdlinker.builtin.ssh_utils:run_cmd:67 - 嘗試對response stdout進行json轉換失敗,原樣輸出
2025-01-03 16:38:03.694 | INFO     | cmdlinker.builtin.logger_operation:console_output:18 - 
================== cmd response  details ==================
status_code : 0
execute_time : 578ms
stdout   : 7393aa6334e6

stderr   : 

2025-01-03 16:38:03.694 | INFO     | __main__:cmd_checker:53 - ========================================開啟根命令Docker合法性檢查========================================
2025-01-03 16:38:03.694 | DEBUG    | __main__:cmd_checker:66 - docker新增非互斥物件:<__main__.Ps object at 0x000001ADA171E9A0>,最新非互斥物件列表:[<__main__.Ps object at 0x000001ADA171E9A0>]
2025-01-03 16:38:03.694 | DEBUG    | __main__:cmd_checker:66 - docker新增非互斥物件:<__main__.Inspect object at 0x000001ADA171EA60>,最新非互斥物件列表:[<__main__.Ps object at 0x000001ADA171E9A0>, <__main__.Inspect object at 0x000001ADA171EA60>]
2025-01-03 16:38:03.694 | DEBUG    | __main__:cmd_checker:66 - docker新增非互斥物件:<__main__.Format object at 0x000001ADA171EAF0>,最新非互斥物件列表:[<__main__.Ps object at 0x000001ADA171E9A0>, <__main__.Inspect object at 0x000001ADA171EA60>, <__main__.Format object at 0x000001ADA171EAF0>]
2025-01-03 16:38:03.694 | INFO     | __main__:cmd_checker:81 - ========================================根命令Docker合法性檢查透過========================================
2025-01-03 16:38:03.694 | INFO     | __main__:exec_cmd:97 - 執行命令列表:['sudo', 'docker', 'inspect 7393aa6334e6\n']
2025-01-03 16:38:03.922 | INFO     | cmdlinker.builtin.logger_operation:console_output:18 - 
================== cmd request  details ==================
execute cmd on host : 47.97.37.176
execute cmd : sudo docker inspect 7393aa6334e6


2025-01-03 16:38:03.922 | INFO     | cmdlinker.builtin.logger_operation:console_output:18 - 
================== cmd response  details ==================
status_code : 0
execute_time : 228ms
stdout   : [
    {
        "Id": "7393aa6334e6a091aae54790f5a6e3505f6dbab9515c5643978afbdc221975dd",
        "Created": "2024-06-26T07:04:49.182101334Z",
        "Path": "docker-entrypoint.sh",
        "Args": [
            "--requirepass",
            "yourpassword"
        ],
        "State": {
            "Status": "running",
            "Running": true,
            "Paused": false,
            "Restarting": false,
            "OOMKilled": false,
            "Dead": false,
            "Pid": 27544,
            "ExitCode": 0,
            "Error": "",
            "StartedAt": "2024-06-26T07:04:49.54446658Z",
            "FinishedAt": "0001-01-01T00:00:00Z"
        },
        "Image": "sha256:7614ae9453d1d87e740a2056257a6de7135c84037c367e1fffa92ae922784631",
        "ResolvConfPath": "/var/lib/docker/containers/7393aa6334e6a091aae54790f5a6e3505f6dbab9515c5643978afbdc221975dd/resolv.conf",
        "HostnamePath": "/var/lib/docker/containers/7393aa6334e6a091aae54790f5a6e3505f6dbab9515c5643978afbdc221975dd/hostname",
        "HostsPath": "/var/lib/docker/containers/7393aa6334e6a091aae54790f5a6e3505f6dbab9515c5643978afbdc221975dd/hosts",
        "LogPath": "/var/lib/docker/containers/7393aa6334e6a091aae54790f5a6e3505f6dbab9515c5643978afbdc221975dd/7393aa6334e6a091aae54790f5a6e3505f6dbab9515c5643978afbdc221975dd-json.log",
        "Name": "/some-redis",
        "RestartCount": 0,
        "Driver": "overlay2",
        "Platform": "linux",
        "MountLabel": "",
        "ProcessLabel": "",
        "AppArmorProfile": "",
        "ExecIDs": null,
        "HostConfig": {
            "Binds": null,
            "ContainerIDFile": "",
            "LogConfig": {
                "Type": "json-file",
                "Config": {}
            },
            "NetworkMode": "default",
            "PortBindings": {
                "6379/tcp": [
                    {
                        "HostIp": "",
                        "HostPort": "6379"
                    }
                ]
            },
            "RestartPolicy": {
                "Name": "no",
                "MaximumRetryCount": 0
            },
            "AutoRemove": false,
            "VolumeDriver": "",
            "VolumesFrom": null,
            "CapAdd": null,
            "CapDrop": null,
            "CgroupnsMode": "host",
            "Dns": [],
            "DnsOptions": [],
            "DnsSearch": [],
            "ExtraHosts": null,
            "GroupAdd": null,
            "IpcMode": "private",
            "Cgroup": "",
            "Links": null,
            "OomScoreAdj": 0,
            "PidMode": "",
            "Privileged": false,
            "PublishAllPorts": false,
            "ReadonlyRootfs": false,
            "SecurityOpt": null,
            "UTSMode": "",
            "UsernsMode": "",
            "ShmSize": 67108864,
            "Runtime": "runc",
            "ConsoleSize": [
                0,
                0
            ],
            "Isolation": "",
            "CpuShares": 0,
            "Memory": 0,
            "NanoCpus": 0,
            "CgroupParent": "",
            "BlkioWeight": 0,
            "BlkioWeightDevice": [],
            "BlkioDeviceReadBps": null,
            "BlkioDeviceWriteBps": null,
            "BlkioDeviceReadIOps": null,
            "BlkioDeviceWriteIOps": null,
            "CpuPeriod": 0,
            "CpuQuota": 0,
            "CpuRealtimePeriod": 0,
            "CpuRealtimeRuntime": 0,
            "CpusetCpus": "",
            "CpusetMems": "",
            "Devices": [],
            "DeviceCgroupRules": null,
            "DeviceRequests": null,
            "KernelMemory": 0,
            "KernelMemoryTCP": 0,
            "MemoryReservation": 0,
            "MemorySwap": 0,
            "MemorySwappiness": null,
            "OomKillDisable": false,
            "PidsLimit": null,
            "Ulimits": null,
            "CpuCount": 0,
            "CpuPercent": 0,
            "IOMaximumIOps": 0,
            "IOMaximumBandwidth": 0,
            "MaskedPaths": [
                "/proc/asound",
                "/proc/acpi",
                "/proc/kcore",
                "/proc/keys",
                "/proc/latency_stats",
                "/proc/timer_list",
                "/proc/timer_stats",
                "/proc/sched_debug",
                "/proc/scsi",
                "/sys/firmware"
            ],
            "ReadonlyPaths": [
                "/proc/bus",
                "/proc/fs",
                "/proc/irq",
                "/proc/sys",
                "/proc/sysrq-trigger"
            ]
        },
        "GraphDriver": {
            "Data": {
                "LowerDir": "/var/lib/docker/overlay2/da926ce47de0e966f295ddf249be649da1f2c1ea162be28fb84a5c5d28db3d96-init/diff:/var/lib/docker/overlay2/cb066f8e71551530105d92d22aee4ac7d10beeff480389676e102be0d701e534/diff:/var/lib/docker/overlay2/b69aadb0a11079daa266a36401ba0ce147450516b500c645eff85176f8e7919b/diff:/var/lib/docker/overlay2/a57e79ac2a99e28c990210b22b831dcd8fad2c98d810de241fc8a3d99b64953a/diff:/var/lib/docker/overlay2/482ff82ce4a7763a75994358255c4e604abab9fcb77fd1f5f2d815a0a98a323b/diff:/var/lib/docker/overlay2/8e951ea76637a9804eb44f89eb8353a5a71da11d304056b22e2d1886e7da9c2d/diff:/var/lib/docker/overlay2/e88bc247f216bd19bd2329b2feeda9cc387df6241598b863b201cafe7ff0db3a/diff",
                "MergedDir": "/var/lib/docker/overlay2/da926ce47de0e966f295ddf249be649da1f2c1ea162be28fb84a5c5d28db3d96/merged",
                "UpperDir": "/var/lib/docker/overlay2/da926ce47de0e966f295ddf249be649da1f2c1ea162be28fb84a5c5d28db3d96/diff",
                "WorkDir": "/var/lib/docker/overlay2/da926ce47de0e966f295ddf249be649da1f2c1ea162be28fb84a5c5d28db3d96/work"
            },
            "Name": "overlay2"
        },
        "Mounts": [
            {
                "Type": "volume",
                "Name": "502c8ea157173ae65d24a2e0173c0e990e5ba1398013ad01a048e7a594a62608",
                "Source": "/var/lib/docker/volumes/502c8ea157173ae65d24a2e0173c0e990e5ba1398013ad01a048e7a594a62608/_data",
                "Destination": "/data",
                "Driver": "local",
                "Mode": "",
                "RW": true,
                "Propagation": ""
            }
        ],
        "Config": {
            "Hostname": "7393aa6334e6",
            "Domainname": "",
            "User": "",
            "AttachStdin": false,
            "AttachStdout": false,
            "AttachStderr": false,
            "ExposedPorts": {
                "6379/tcp": {}
            },
            "Tty": false,
            "OpenStdin": false,
            "StdinOnce": false,
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
                "GOSU_VERSION=1.12",
                "REDIS_VERSION=6.2.6",
                "REDIS_DOWNLOAD_URL=http://download.redis.io/releases/redis-6.2.6.tar.gz",
                "REDIS_DOWNLOAD_SHA=5b2b8b7a50111ef395bf1c1d5be11e6e167ac018125055daa8b5c2317ae131ab"
            ],
            "Cmd": [
                "--requirepass",
                "yourpassword"
            ],
            "Image": "redis",
            "Volumes": {
                "/data": {}
            },
            "WorkingDir": "/data",
            "Entrypoint": [
                "docker-entrypoint.sh"
            ],
            "OnBuild": null,
            "Labels": {}
        },
        "NetworkSettings": {
            "Bridge": "",
            "SandboxID": "b98966d73f26b520343cc69867d8ac56dd3ac65655375c2edb047cabc9eec9c8",
            "HairpinMode": false,
            "LinkLocalIPv6Address": "",
            "LinkLocalIPv6PrefixLen": 0,
            "Ports": {
                "6379/tcp": [
                    {
                        "HostIp": "0.0.0.0",
                        "HostPort": "6379"
                    }
                ]
            },
            "SandboxKey": "/var/run/docker/netns/b98966d73f26",
            "SecondaryIPAddresses": null,
            "SecondaryIPv6Addresses": null,
            "EndpointID": "63a20510e954d1b81181d319b2d004e68918a2926d3db1e71e846197b79c72e1",
            "Gateway": "172.17.0.1",
            "GlobalIPv6Address": "",
            "GlobalIPv6PrefixLen": 0,
            "IPAddress": "172.17.0.2",
            "IPPrefixLen": 16,
            "IPv6Gateway": "",
            "MacAddress": "02:42:ac:11:00:02",
            "Networks": {
                "bridge": {
                    "IPAMConfig": null,
                    "Links": null,
                    "Aliases": null,
                    "NetworkID": "7fdaf78d05fd8a3d1c3c2ae0151022ae8c326582524327e18403fdab4a26f16d",
                    "EndpointID": "63a20510e954d1b81181d319b2d004e68918a2926d3db1e71e846197b79c72e1",
                    "Gateway": "172.17.0.1",
                    "IPAddress": "172.17.0.2",
                    "IPPrefixLen": 16,
                    "IPv6Gateway": "",
                    "GlobalIPv6Address": "",
                    "GlobalIPv6PrefixLen": 0,
                    "MacAddress": "02:42:ac:11:00:02",
                    "DriverOpts": null
                }
            }
        }
    }
]
stderr   : 
Process finished with exit code 0

我們可以很方便的使用返回的 status_code/stdout/stderr 進行請求響應斷言


OpenSourceTest cmdlinker 社群
歡迎小夥伴加群,討論 cmdlinker 相關問題,或提出最佳化建議!
QQ 群(自動化測試 - 夜行者):816489363

相關文章