
上一篇文章把sonic-mgmt的环境和基础配置跑通之后不少朋友私信问我同一个问题环境有了测试用例到底该怎么写这问题问得挺实在因为sonic-mgmt这个仓库跟普通pytest工程不太一样它把设备操作、数据面验证、配置下发全揉在一起。光是弄明白duthost、ptfhost这几个fixture就得花不少时间。这篇系列3就专门讲测试用例代码怎么写从最基础的设备命令断言到功能测试用例设计方法再到接口测试用例和数据面回包验证尽量一条线走完让后面的人少踩一些坑。先交代清楚一件事sonic-mgmt里的测试用例本质上是pytest用例但它又叠了一层Ansible的设备抽象。也就是说测试函数里拿到的duthost不是普通字典而是一个能直接执行命令、下发配置的设备对象。理解这一点后面所有代码都好写了。1. 测试用例在sonic-mgmt仓库里的组织逻辑不看文档也能找到入口1.1 测试目录和命名规则第一次打开sonic-mgmt仓库的人通常会被它的目录结构吓到。根目录下有一堆ansible目录、tests目录、scripts目录看起来像是把好几个项目塞在了一起。但实际上测试用例的主战场就在tests目录里。tests目录下面有几类东西值得重点关注tests/common/所有公共fixture、工具函数、常量定义都在这比如duthost、ptfhost这些核心fixture以及各种正则匹配工具。tests/feature/按功能模块划分的测试用例目录比如tests/bgp/、tests/lacp/、tests/vlan/里面就是一个个test_*.py文件。tests/scripts/一些独立运行的辅助脚本不归pytest管比如起服务、清配置用的。tests/test_*.py散落在根目录的用例通常是跨模块的冒烟测试。命名规则跟标准pytest一致测试文件以test_开头测试函数以test_开头。但sonic-mgmt多了一个约定就是每个feature目录下基本都会有一个conftest.py专门放这个feature专属的fixture。比如tests/bgp/conftest.py里会有bgp_sessions、bgp_neighbors之类的fixture方便同一模块多个用例复用。如果你接手一个陌生模块我建议先别直接翻用例代码而是先看这个目录下有没有conftest.py再看tests/common/plugins/里有没有对应的插件。sonic-mgmt把大量逻辑放在pytest插件里比如pytest_ansible、pytest_checkpoint、pytest_rpc这些不了解插件机制的话你会觉得用例跑起来像变魔术也不知道前置条件到底是从哪注入的。1.2 conftest.py的fixture加载链路sonic-mgmt的fixture加载链路比较复杂上手时最容易困惑。简单说pytest在收集用例时会把根目录和当前测试目录的所有conftest.py按层级加载。你写用例时能直接用duthost是因为在tests/conftest.py里已经注册了一个名为duthost的fixture而这个fixture本身又依赖testbed、inventory这些更底层的fixture。一条典型的加载链路长这样pytest_configure - 加载 ansible_* 插件 - 读取 inventory 文件 - 构建 testbed 对象 - duthost fixture 从 testbed 中拿出 DUT 主机对象 - 测试函数拿到 duthost执行命令/下发配置所以你平时写用例时最常干的事就是在测试函数参数里声明要用哪些fixturedef test_check_interface(duthost): output duthost.shell(show interfaces status)[stdout] assert Ethernet0 in outputduthost通过shell()方法返回一个Ansible执行结果字典里面包含stdout、stderr、rc这些字段。这个设计跟直接SSH到设备上敲命令最大的区别是它天然支持对多台设备并发操作而且返回结果可编程化处理方便后续断言。这里有个容易踩的坑不要在用例里直接用duthost.host.options[inventory_manager]这种底层API去拿设备信息。不同版本的ansible升级后这些内部结构变化很大一旦升级就全挂。更稳妥的做法是用sonic-mgmt封装好的duthost对象上的方法比如duthost.shell()、duthost.command()、duthost.facts。2. 第一个用例从“登录设备执行命令”到可断言的pytest用例2.1 duthost fixture到底给了你什么我刚接触sonic-mgmt的时候最大的困惑就是duthost到底是个什么类型。它看起来像个Ansible主机对象但又能直接调用很多设备操作方法。后来才知道它是sonic-mgmt在tests/common/fixtures/duthost_utils.py里封装的一个增强版对象底层还是Ansible的AnsibleHost但在上面加了大量SONiC相关的快捷方法。它最常用的能力有三类执行任意命令比如duthost.shell(show version)返回结果的dict里能取stdout、stderr、rc。进入配置模式或操作config_db比如duthost.command(config vlan add 100)。读取设备facts比如duthost.facts[platform]、duthost.facts[asic_type]这些在跳过不支持的平台时特别有用。写用例时这三个能力基本覆盖了90%的场景。我一开始也纠结过到底用duthost.command()还是duthost.shell()后来发现两者的区别不大command()更适合执行需要检查返回码的命令如果命令执行失败它会直接抛异常而shell()更像传统的shell执行不管rc结果全靠你自己断言。所以我在断言系统状态时喜欢用command()在需要容忍部分命令失败时用shell()。2.2 最小可用用例与运行命令说再多理论不如直接写一个能跑的最小用例。假设我们要验证设备的show version能正常返回SONiC版本信息用例可以写成这样# tests/smoke/test_show_version.py import pytest pytestmark [ pytest.mark.topology(any), pytest.mark.sanity_check(skip_sanityTrue), ] def test_show_version(duthost): result duthost.command(show version) assert result[rc] 0 assert Software in result[stdout]注意这里用了两个markertopology(any)表示这个用例不挑拓扑sanity_check(skip_sanityTrue)表示跑用例前不用做全量sanity检查适合本地快速验证。这两个marker是sonic-mgmt内部约定不写的话默认行为可能会执行额外的前置检查让你误以为用例卡住了。运行用例的命令基本长这样cd sonic-mgmt/tests pytest --inventory ../ansible/inventory \ --host-pattern dut-name \ --module-path ../ansible/library \ --user admin \ test_smoke.py::test_show_version参数里--host-pattern填的是inventory里的设备名--module-path指向sonic-mgmt自带的ansible模块库。如果你在本地已经跑过sonic-mgmt自带的sanity测试那么这些参数应该不陌生。跑通之后你就拥有一个可反复执行的用例模板了。2.3 常见运行错配与排查思路写第一个用例时会遇到几个很典型的问题我列一下最常见的报错host not found in inventory说明--host-pattern填的设备名在inventory文件里不存在检查一下inventory里的hostname是否跟/etc/hosts或ansible的hostvars一致。报错duthost not found说明你的pytest没有加载tests/conftest.py里的fixture最可能是当前工作目录不对pytest没有把tests目录作为rootdir。建议在tests目录下运行或者用--rootdirtests指定。报错ssh connection refused很多情况下是设备SSH key没加进~/.ssh/known_hosts或者--user参数对应的用户没有免密登录权限。用例collect失败检查文件命名是否以test_开头函数是否在class内且类名以Test开头。这些问题看起来零碎但几乎人人都会遇到。我建议跑第一个用例时先把命令简化到最小不要加任何自定义选项确保基础链路通了你再加参数。3. 功能测试用例设计方法把网络特性拆成“配置、状态、流量”三个维度3.1 用等价类和边界值划分测试点很多人拿到一个功能特性后不知道从哪里开始写用例。比如要测VLAN功能第一反应是加个VLANping一下完事。这当然能跑但覆盖度不够回头出了问题还得重新补用例。我自己习惯的测试用例设计方法是把功能点按等价类划分再找边界值。拿VLAN举例一个网管只关心三个维度配置入参、设备状态、转发行为。配置入参的等价类包括VLAN ID合法值1-4094、非法值0、4095、负数、超范围、字符串类型、重复创建、删除不存在的VLAN等。每个等价类对应一个用例点。设备状态的等价类包括VLAN创建后是否出现在show vlan输出中、端口加入VLAN后是否变成untagged/tagged模式、VLAN deletion后是否从数据库消失等。转发行为的等价类包括同VLAN内两台主机能否互通、不同VLAN主机是否隔离、trunk port是否只放行指定VLAN等。把这三类点列出来后你就不会只写一个ping通就交差了。用表格整理一下结构更清晰测试维度输入/操作预期结果断言方式配置入参创建VLAN 100命令成功返回码0duthost.command rc配置入参创建VLAN 0命令失败返回非0异常 or rc检查配置入参删除不存在的VLAN 200返回错误提示输出匹配错误信息设备状态查看VLAN列表VLAN 100存在show vlan输出匹配转发行为同VLAN内ping通PTF回包验证转发行为跨VLAN ping不通PTF验证没有回包这种表格既是设计文档也是后面写用例的蓝图。我通常先把表格填好再照着表格逐条写测试函数比自己边写边想效率高很多。3.2 配置下发、状态校验、行为验证三种断言的配合功能测试用例最常见的写法是操作-验证模式但在网络设备场景里验证不能只看一条。建议把断言拆成三层第一层是操作本身的结果。比如下发config vlan add 100后命令返回码是不是0。这个断言能抓住命令语法错误、权限不足这类问题。第二层是设备状态。比如创建VLAN后用show vlan或直接查redis数据库确认配置真的生效了。SONiC的配置最终会写进CONFIG_DB所以你可以用redis-cli -n 4去查def test_vlan_added_to_config_db(duthost): duthost.command(config vlan add 100) result duthost.shell(redis-cli -n 4 hgetall VLAN_TABLE|Vlan100)[stdout] assert result ! 这段代码的意思是先创建VLAN 100然后从CONFIG_DB中读VLAN_TABLE|Vlan100如果返回空就说明配置没写进去。第三层是行为验证。也就是真正发流量验证数据面行为是否符合预期。这一步一般交给PTF来完成后面我会详细讲。三层断言都过了才能算一个完整的功能测试用例。我见过不少只做第一层断言的用例结果命令成功执行了业务其实没生效这种用例的守护作用基本等于零。4. 设备接口测试用例CLI、config_db和网管接口的适配差异4.1 不同操作层的选择SONiC设备对外交互的接口不止一个有CLI命令、有config_db数据库、有REST/gNMI接口。测试用例要覆盖哪个层取决于你的被测对象是哪个。很多新手容易混在一起在测试CLI的用例里直接查数据库然后抱怨查不到。我自己的做法是分层写接口测试用例纯CLI层用例只验证命令行工具的解析、输出、错误提示是否正确不关心配置是否已经写库。配置层用例验证config命令或sonic-cfggen生成的配置是否写入CONFIG_DB以及各进程是否正确加载。网管接口层用例通过REST/gNMI接口下发配置、查询状态验证接口返回的数据结构和真实设备状态一致。拿接口测试用例举例如果你想验证REST接口能查询到一个已配置的VLAN那就要先通过CLI或config_db创建VLAN再调用REST接口去读断言返回的JSON里包含该VLAN信息。import requests def test_vlan_rest_api(duthost, testbed): # 1. 先通过CLI创建VLAN duthost.command(config vlan add 100) # 2. 构造REST请求 mgmt_ip duthost.host.options[inventory_manager].get_host(duthost.hostname).vars[ansible_host] url fhttps://{mgmt_ip}/restconf/data/sonic-vlan:sonic-vlan/VLAN_LIST response requests.get(url, auth(admin, password), verifyFalse) # 3. 断言返回的VLAN列表包含Vlan100 assert Vlan100 in response.text实际项目里我不会在用例里直接写requests.getsonic-mgmt的tests/common/plugins里已经封装好了rest相关fixture直接用更省事。但这段代码能帮你理解接口测试用例的套路先准备前置条件再调用被测接口最后断言接口行为。4.2 接口用例的返回码与数据校验接口测试用例跟普通功能用例最大的差异在于你要校验的是接口契约而不仅仅是设备表现。这意味着断言不能只看返回码还要检查返回的HTTP状态码是否符合预期200/201/400/404等。返回的数据结构是否规范字段名是否跟接口文档一致。返回值跟设备实际状态是否一致比如REST返回Vlan100你用show vlan查到的也是Vlan100。我习惯在用例里定义一个小工具函数把接口返回的数据标准化后再断言避免在多个用例里重复写解析逻辑。比如def get_vlan_list(rest_client): raw rest_client.get(/restconf/data/sonic-vlan:sonic-vlan/VLAN_LIST) vlan_list raw.json().get(sonic-vlan:VLAN_LIST, {}).get(VLAN_LIST, []) return [v[vlan_id] for v in vlan_list]有了这个工具函数测试用例本身就能写得非常简洁def test_rest_vlan_created(duthost, rest_client): duthost.command(config vlan add 100) assert 100 in get_vlan_list(rest_client)接口测试用例要特别注意一点绝对不能在用例里调整被测接口的返回值。有人图省事直接改mock数据让断言通过这完全背离了测试目的。网络设备测试强调真实性宁可先fail再排查也不要伪造结果。5. 数据面验证用PTF写回包测试用例5.1 PTF在SONiC测试中的位置功能测试用例用命令能解决一部分问题但网络设备的核心是转发布线光看控制面状态不够。你怎么知道VLAN配置好了之后端口真的把报文从正确的接口转出去了这就需要数据面验证。sonic-mgmt的数据面验证主要靠PTFPacket Test Framework。PTF是一个基于Python的报文测试框架它可以在测试主机上构造报文从某个端口发进DUT同时监听另一个端口有没有收到预期报文。说白了就是一个可编程的、更灵活的高级抓包工具。在sonic-mgmt里跑PTF测试通常有两种方式把PTF用例放在tests/feature/ptf_test/目录下独立运行。直接在pytest用例里调用PTF的dataplane对象把普通用例和数据面验证混在一起写。第二种方式用起来更灵活推荐优先掌握。在pytest用例中你可以通过ptfadapter这个fixture拿到一个PTF测试适配器然后构造报文、发送报文、验证回包。5.2 简单回包用例验证VLAN内互通下面是一个简化版的VLAN成员互通用例。假设DUT上有两个端口属于VLAN 100测试主机分别连着这两个端口。我们从端口1发一个ICMP请求验证从端口2能不能收到转发出来的报文。import ptf.testutils as testutils from ptf.mask import Mask def test_vlan_ping(ptfadapter, duthost, ptfhost): # 先通过DUT下发VLAN配置并把两个端口加入VLAN duthost.command(config vlan add 100) duthost.command(config vlan member add 100 Ethernet0) duthost.command(config vlan member add 100 Ethernet4) # 构造一个简单的ICMP请求报文源MAC、目的MAC按实际拓扑填充 pkt testutils.simple_icmp_packet( pktlen98, eth_src00:11:22:33:44:55, eth_dst00:aa:bb:cc:dd:ee, ip_src10.0.0.1, ip_dst10.0.0.2, icmp_type8, icmp_code0, ) # 从PTF端口0发出期望在PTF端口1收到同一份报文 testutils.send_packet(ptfadapter, 0, pkt) testutils.verify_packet(ptfadapter, pkt, 1)这段代码里的关键点是testutils.send_packet和testutils.verify_packet。发送和接收的端口编号取决于你的PTF拓扑和testbed文件不能瞎写。通常在sonic-mgmt里PTF端口编号跟DUT端口名有对应关系可以在testbed.csv或ptf_topology配置里查看。如果只想验证报文是否被丢弃可以用verify_no_packet比如跨VLAN隔离测试def test_vlan_isolation(ptfadapter, duthost): duthost.command(config vlan add 200) duthost.command(config vlan member add 200 Ethernet4) # 目的IP在VLAN 100而源端口属于VLAN 200应该收不到回包 pkt testutils.simple_icmp_packet(...) testutils.send_packet(ptfadapter, 0, pkt) testutils.verify_no_packet(ptfadapter, pkt, 1)这里有个小诀窍verify_no_packet一定要设置足够的超时时间不然因为报文还没跑到目标端口你就断言没有收到会产出假阳性。我自己习惯在回包验证前先sleep 2秒或者调整verify_no_packet的timeout参数保证数据面状态稳定。5.3 PTF用例调试心得PTF用例跑不通时先别急着改代码按下面顺序排查检查端口映射对不对。很多回包验证失败都是因为端口编号选错了报文从完全无关的端口发出去自然收不到。查看PTF主机日志。PTF会把收发报文的记录写在对应日志里用tail -f /tmp/ptf.log能实时看到有没有报文进出。在DUT侧抓包确认。用tcpdump -i Ethernet0 icmp在DUT端口上抓包看报文是否真的到了DUT有没有从正确端口转发出去。确认DUT的MAC地址学习正常。VLAN转发依赖MAC表如果设备没有正确学习到PTF端口的MAC回包也会失败。PTF用例跑通后你会对数据面验证产生很强的依赖感。我现在几乎每个涉及具体业务的用例都会附带一两个PTF断言因为控制面看着再正常数据面不通就是不要用。6. 把用例跑进日常回归参数化、标记与排障技巧6.1 参数化和标记避免“复制粘贴”式用例网络功能测试天然适合参数化。比如要测试多个VLAN的创建行为与其写三个几乎一样的函数不如用一个参数化用例import pytest pytest.mark.parametrize(vlan_id, [100, 200, 300]) pytest.mark.topology(t0) def test_vlan_create(duthost, vlan_id): duthost.command(fconfig vlan add {vlan_id}) result duthost.shell(fshow vlan id {vlan_id}) assert str(vlan_id) in result[stdout]参数化不仅省代码更重要的是让测试报告更清晰每条参数对应一条独立的测试记录哪个VLAN挂了一眼就能看到。标记marker的用法同样重要。sonic-mgmt常用标记有topology、ptf、qos、platform等。你写用例时一定要根据实际需求打标记否则后面做回归筛选时很难过滤。比如只跑PTF相关用例可以这样pytest -m ptf tests/如果只跑拓扑为t0的用例pytest -m topology(t0) tests/6.2 把用例集成到回归流程时的几个坑把用例跑进自动回归后你会遇到一些单跑时不会暴露的问题。最典型的就是用例间的状态污染。一个用例创建了VLAN 100跑完没清理下一个用例假设环境是干净的结果断言全崩。解决办法有两个一是在用例结尾用try/finally或fixture清理状态二是确保每个用例都主动构造前置条件而不是依赖上一个用例的残留状态。我更喜欢第二种因为它对执行顺序的容忍度更高。第二个坑是并发执行。sonic-mgmt本身支持pytest-xdist并发但并发跑网络用例时要小心因为多个worker同时操作同一台DUT可能互相干扰。我的建议是涉及配置变更的用例尽量串行只读类的状态查询可以并发。或者用pytest.mark.parametrize配合xdist_group控制执行组。第三个坑是跳过条件不完整。比如某些用例只在特定ASIC上支持如果你没写跳过逻辑跑到不支持的平台就直接失败而不是优雅跳过。推荐用pytest.mark.skipif配合duthost.facts判断pytest.mark.skipif( duthost.facts[asic_type] not in [mellanox], reasonOnly supported on Mellanox platform ) def test_mellanox_specific_feature(duthost): ...6.3 用例失败时的排障顺序最后聊一下用例失败后怎么排查。我自己固定在下面几条链路里找问题先看pytest输出里的失败断言明确是命令执行失败、状态不匹配、还是回包超时。再登到DUT上手动执行同一条命令看设备本身是否处于预期状态。如果手动执行也失败那说明前置配置没生效问题在用例的依赖上。然后看DUT的日志重点是/var/log/syslog和对应的容器日志。SONiC各功能进程跑在不同容器里比如bgpd在bgp容器teamd在team容器用docker logs能查到详细报错。最后才是怀疑用例本身写错了。不要一上来就改用例先确定设备和环境没问题再改代码。还有一个很实用的技巧跑用例前先pytest --collect-only看看用例收集结果提前发现import错误、fixture缺失这些低级问题能省下大量跑测时间。说了这么多其实核心就一句话写sonic-mgmt测试用例重点不是写多少行代码而是把每个用例拆成配置、状态、行为三件套再配上合适的断言和清理逻辑。我自己在项目里坚持这个思路后用例的可维护性提升了一大截回归跑出来的失败项也基本都是真实问题不再是被环境或前序用例带崩的误报。如果你正在为SONiC-mgmt写测试用例建议从小功能开始先把命令断言和状态校验跑通再逐步引入PTF回包测试最后再规划参数化和CI集成一条路走下来会顺手很多。