RTMP 连接单元测试之旅

从一条 RTMP 连接开始创建单元测试

  想象我们正在检查一条刚刚建立的 RTMP 连接。它要先准备好默认状态,随后把控制消息写到网络上;如果调用者把连接对象弄丢了,它还得明确报错。我们不必真的连上服务器,就能在本地把这些行为逐项验证出来。

  这就是 net_connection_test.go 的故事:用 Go 的表驱动测试,借助一根内存里的"假网线",检查连接初始化和消息写出的结果。

第一:测试从哪里进入?

  文件开头声明:

go 复制代码
package rtmp_test

  测试使用外部测试包形式,而不是与被测包共享未导出的内部实现。随后它给 Monibuca 的 RTMP 包起了一个简短的别名:

go 复制代码
rtmp "m7s.live/v5/plugin/rtmp/pkg"

  因此测试调用的是 rtmp.NetConnection、rtmp.NewNetConnection 等公开 API。当前工作目录的 go.mod 把 m7s.live/v5 作为依赖,所以从项目根目录执行 go test . 时,Go 会编译这里的测试并解析该依赖。

  这里有一个重要边界:工作区根目录没有 Monibuca 的 net_connection.go 实现。测试文件虽名为 net_connection_test.go,实际验证的是依赖包公开出来的行为,并非直接编译本地的一份 net_connection.go 源码。

第二:搭一条不会出门的网线

  测试连接的准备工作集中在 newTestNetConnection:

go 复制代码
peer, conn := net.Pipe()
nc := rtmp.NewNetConnection(conn)
nc.Context = context.Background()

net.Pipe() 返回一对彼此连接的内存端点。conn 交给 NetConnection,测试则握着 peer。于是连接发出的字节会出现在 peer 上,不需要端口、不需要服务器,也不会受到真实网络波动影响。

  这根"网线"有个不太显眼的特性:读写是同步配合的。若测试主协程直接调用发送函数,发送方可能等待读取端,而读取又还没开始,双方就会互相等待。因此后面的写出测试会把发送放进 goroutine,再由主协程从 peer 读取。

  辅助函数还做了两件收尾工作:

  • 设置 nc.Context,让测试连接拥有明确的上下文。
  • 用 t.Cleanup 关闭对端并调用 nc.Dispose(),确保每个子测试结束时释放资源。

t.Helper() 则让辅助函数出错时,Go 测试报告更接近实际调用它的测试位置。

第三:新连接应该带着什么默认值?

  TestNewNetConnectionDefaults 不只检查一个字段,而是把要检查的项目放进表里:

go 复制代码
tests := []struct {
    name string
    get  func(*rtmp.NetConnection) int
    want int
}{ ... }

  表中的每一行描述一个情形:name 给子测试命名,get 负责读取目标字段,want 是预期值。当前表格分别检查读取和写入的 chunk 大小是否等于 RTMP_DEFAULT_CHUNK_SIZE。

  随后,循环通过 t.Run(tt.name, ...) 把每一行变成独立子测试。这样的表驱动写法有两个好处:

  1. 两个检查共享相同的执行逻辑,减少重复代码。
  2. 某项失败时,报告会指出具体是"read chunk size"还是"write chunk size"。

  这也是扩展测试的常见路径:增加一个表格用例,而不是复制整段测试流程。

第四:让消息真正走过网线

  TestSendMessageWritesControlChunk 要回答的问题是:调用 SendMessage 后,连接究竟写出了哪些字节?

  表格为两种 ObjectEncoding 设置准备输入消息和期望字节。每个子测试按以下顺序工作:

  1. 创建一条新的内存连接。
  2. 设置当前用例的 ObjectEncoding。
  3. 给对端读取设置一秒的截止时间,避免没有输出时无限等待。
  4. 在 goroutine 中调用 SendMessage。
  5. 用 io.ReadFull 读出期望长度的完整数据。
  6. 检查发送错误,并用 bytes.Equal 逐字节比较实际输出与预期输出。

  测试中的发送动作如下:

go 复制代码
sendDone := make(chan error, 1)
go func() {
    sendDone <- nc.SendMessage(rtmp.RTMP_MSG_ACK, tt.message)
}()

  带缓冲的 sendDone 用于把发送结果交回主测试协程。此处 goroutine 不只是为了并发,而是配合 net.Pipe 的同步读写特性,避免发送先开始后阻塞、导致读取永远无法启动。

  期望字节数组把测试提升到了协议输出层:它不只断言"函数返回成功",还验证 chunk 的头部、消息类型和负载确实以预期形式写出。以这些用例为例,数组包含 chunk 基本头、消息头和 4 字节消息体;消息体使用特定字节值,便于发现字节顺序或编码结果变化。

  这里有一个值得留意的测试命名边界。 用例名写着 "AMF0 encoding" 和 "AMF3 encoding",但用例传入的是 Uint32Message,断言的是 ACK 控制消息写出的字节;它们并没有直接验证 AMF0/AMF3 对象序列化格式。更准确地说,现有测试覆盖了两种 ObjectEncoding 设置下这条控制消息的写出结果。若目标是验证 AMF 编码本身,还需要构造 AMF 命令/对象消息,并对其编码后的消息体单独断言。

第五:用户控制消息也要留下脚印

  TestSendUserControlWritesExpectedChunk 沿用相同的表驱动结构,这次覆盖 ping request 和 ping response:

  • eventType 是输入的用户控制事件类型。
  • want 是完整预期 chunk。
  • 子测试启动 SendUserControl,从 pipe 对端读取数据,再逐字节比较。

  两个用例的消息类型都是 RTMP_MSG_USER_CONTROL,消息体则以两个字节表示事件类型。测试直接比较线上的完整字节,因此若事件类型、消息体长度或消息类型发生意外变化,错误会在这里暴露。

  这一幕也展示了良好的测试复用方式:连接创建、超时保护、异步发送和完整读取这些步骤与上一幕相同;变化的只有用例输入和预期输出。

第六:没有连接对象时,错误不能被吞掉

  最后,TestSendMessageRejectsNilReceiver 把连接指针设为 nil,分别传入 ACK 和用户控制消息,再确认 SendMessage 返回了错误:

go 复制代码
var nc *rtmp.NetConnection
if err := nc.SendMessage(rtmp.RTMP_MSG_ACK, tt.message); err == nil {
    t.Fatal("SendMessage() error = nil, want an error for a nil receiver")
}

  这是一个负向测试:它验证异常输入下的行为,而不是正常消息路径。表格让两类消息都通过同一条 nil receiver 检查逻辑,失败信息也能指出是哪种消息用例没有得到预期错误。

重点、难点与亮点回顾

重点:验证可观察行为

  测试关心的是公开 API 的结果:默认字段值、写到网络端的字节,以及无效 receiver 时返回的错误。对网络协议代码而言,字节级断言通常比"没有报错"更能说明输出是否正确。

难点:同步 pipe 与 goroutine 配合

  net.Pipe 不像缓冲队列那样让一端可以随意写入;读写双方需要配合。先启动发送 goroutine,再在测试协程中读取,是这些测试可以稳定完成的关键。读取截止时间则为失败情形提供了超时边界。

亮点:表驱动与子测试

  每个测试把变化的数据放在表格里,把共同行为保留在循环中。t.Run 让每个用例单独显示、单独失败,既避免重复,也方便将来继续添加协议边界用例。

需要知道:日志与 -v

  当前文件没有调用 t.Log 或 t.Logf,因此测试本身不会输出自定义步骤日志。运行:

powershell 复制代码
go test -v .

  可以看到 Go 测试框架提供的测试函数、子测试开始与通过/失败信息;若要查看每个步骤中的字节内容,则需要在相应测试中显式加入 t.Logf。

如何运行这段故事?

  在项目根目录执行:

powershell 复制代码
go test .

  想看到每个测试和子测试的执行过程,则执行:

powershell 复制代码
go test -v .

  通过后,最重要的结论是:当前根目录中的表驱动测试可以编译运行,并验证 Monibuca RTMP 依赖包的这些公开行为。它们不是对本地 net_connection.go 文件的直接测试;若未来把 RTMP 实现源码纳入当前模块,测试导入和包结构也应相应调整,才能直接覆盖本地实现。

相关推荐
阿明副业观察2 小时前
AI视频生成工具:功能、特点与高效制作攻略
人工智能·音视频
weixin_690654742 小时前
龙迅#LT7911EXS 高性价比IC,功能适用于HDMI/DP/TPYE-C转单PORT MIPICSI/DSI ,分辨率高达4K60HZ。
嵌入式硬件·音视频·信号处理
Likeadust3 小时前
应急调度不再碎片化!一套视频会议/直播/点播/集群对讲EasyDSS打造一体化音视频指挥平台
音视频·媒体·easydss
Android系统攻城狮16 小时前
Linux Gstreamer深度解析之gst_audio_encoder_get_frame_samples_min调用流程与实战(六十)
linux·运维·服务器·音视频·gstreamer音视频·音视频进阶·gstreamer音视频进阶
黑妹天下第一乖16 小时前
第09讲 · 多媒体与音频 SDK:硬件编解码与端侧语音
人工智能·嵌入式硬件·深度学习·机器人·音视频·iot
ken223218 小时前
(aaa) piper TTS 文字转语音 安装、使用;与 calibre 电子书朗读 (****)
音视频
我就是不信21 小时前
CuTest:轻量级 C 语言单元测试框架实战指南
c语言·单元测试·log4j
开开心心就好1 天前
本地文件搜索工具推荐,占用小速度还快
智能手机·ffmpeg·ocr·word·音视频·网络攻击模型·安全架构