28、测试与调试:单元测试、UI测试、网络模拟测试、日志系统

说实话,做WebRTC视频会议开发,最让人头疼的不是写代码,而是调试。音视频这东西,牵一发而动全身。网络抖一下,画面卡了;内存没管好,App闪退了;信令发慢了,通话建立失败。我早期做的一个项目,上线前自测一切正常,结果用户一多,各种诡异问题全冒出来了。

后来我学乖了。测试和调试必须从一开始就嵌入到开发流程里。这一章,我就把我在Android WebRTC项目里积累的测试调试经验,掰开了揉碎了讲给你听。

单元测试:把每个模块锁死在牢笼里

单元测试,说白了就是给每个函数、每个类单独做体检。你写了一个信令解析器,能不能正确解析JSON?你写了一个网络状态机,状态切换对不对?这些都要测。

我个人习惯用 JUnit + Mockito 的组合。JUnit负责跑测试,Mockito负责模拟那些难搞的依赖——比如WebRTC的PeerConnection,你总不能真的去连一个远端吧?

举个例子,测试信令处理逻辑:

class SignalingManagerTest {

    @Mock
    private WebSocketClient mockWebSocket;

    @InjectMocks
    private SignalingManager signalingManager;

    @Test
    fun testHandleOfferMessage() {
        // 模拟收到offer
        val offerJson = """{"type":"offer","sdp":"v=0..."}"""
        signalingManager.handleMessage(offerJson)

        // 验证是否调用了setRemoteDescription
        verify(mockWebSocket, times(1)).sendMessage(anyString())
    }
}

这里有个坑,我踩过。WebRTC的很多API是异步的,比如setRemoteDescription。你如果直接在主线程里跑测试,回调还没执行,测试就结束了。解决办法是用 CountDownLatch 或者 RxJava的TestScheduler 来等待异步结果。

我的小技巧: 写单元测试时,尽量把业务逻辑和WebRTC原生API解耦。封装一层接口,测试时用Mock对象替换。这样你的测试跑得快,而且不依赖真机。

UI测试:别让界面骗了你

UI测试,很多人觉得不重要。你想想看,视频会议App里,按钮点下去有没有反应?画面渲染出来是不是黑的?这些肉眼可见的问题,如果靠人工去测,一次两次还行,每次发版都测一遍,谁受得了?

Android上做UI测试,我推荐 EspressoUI Automator。Espresso适合测单个Activity里的交互,UI Automator可以跨App操作,比如模拟来电。

但WebRTC的UI测试有个特殊之处——视频流是动态的。你不能像测一个TextView那样,直接assert文本内容。那怎么办?

我常用的方法是:检测SurfaceView的渲染状态。WebRTC渲染视频时,SurfaceView会持续收到帧数据。你可以写一个自定义的IdlingResource,等SurfaceView的帧数超过某个阈值,再断言UI元素的位置和大小。

class VideoRenderIdlingResource(private val surfaceView: SurfaceView) : IdlingResource {

    private var isIdle = false

    override fun getName() = "VideoRenderIdling"

    override fun isIdleNow() = isIdle

    override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
        surfaceView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
            if (surfaceView.width > 0 && surfaceView.height > 0) {
                isIdle = true
                callback?.onTransitionToIdle()
            }
        }
    }
}
注意: UI测试不要依赖真实的网络连接。测试环境里,网络延迟和丢包会干扰测试结果。建议用本地回环地址(127.0.0.1)或者模拟的MediaStream。

网络模拟测试:在实验室里复现真实网络

做WebRTC开发,最怕的就是「我这边好好的,用户那边卡成PPT」。网络环境千差万别,WiFi、4G、弱网、高延迟……你不可能每个场景都去实地测试。

所以,网络模拟测试是必备技能。Android上我常用 Facebook的Augmented Traffic Control(ATC),或者直接用 Linux的tc命令 来模拟网络条件。

具体做法是:在测试手机上开启一个VPN服务,通过ATC控制这个VPN的带宽、延迟、丢包率。然后跑自动化测试,验证在不同网络下,视频分辨率、码率、卡顿次数的变化。

我曾经遇到过一个案例:在WiFi下通话正常,但切换到4G后,视频经常卡住不动。后来用ATC模拟了4G的延迟和抖动,发现是拥塞控制算法对网络变化反应太慢。调整了 GoogCcNetworkController 的参数后,问题解决了。

这里给出一份常用的网络模拟参数表:

网络场景 带宽 延迟 丢包率 抖动
理想WiFi 50 Mbps 10 ms 0% 0 ms
普通4G 10 Mbps 50 ms 1% 20 ms
弱网3G 1 Mbps 200 ms 5% 100 ms
极端丢包 2 Mbps 100 ms 15% 50 ms

日志系统:调试的最后一根救命稻草

单元测试和UI测试覆盖了正常流程,网络模拟测试覆盖了异常网络。但总有一些bug,是测试环境复现不了的——比如用户手机内存不足、系统杀进程、或者某个奇怪的Android厂商定制ROM。

这时候,日志系统就是你唯一的线索。WebRTC本身有丰富的日志输出,但默认是关闭的。你需要手动开启:

// 开启WebRTC内部日志
Logging.enableLogToDebugOutput(Logging.Severity.LS_INFO);

// 设置日志文件路径
Logging.setLogFile("/sdcard/webrtc_log.txt");

但光有WebRTC的日志还不够。我习惯在App层也打一套日志,记录关键事件的时间戳:

  • 信令连接建立时间
  • ICE候选收集完成时间
  • 视频首帧渲染时间
  • 网络切换事件
  • CPU和内存使用率

这些日志配合WebRTC内部的 RTCStats,基本能定位90%的问题。RTCStats会输出每秒钟的发送码率、接收码率、丢包率、往返时间等指标。你可以把这些数据上传到后台,做可视化分析。

核心思路: 日志不是写给机器看的,是写给人看的。每条日志都要包含「时间、线程、标签、关键数据」。不要打那种「enter method xxx」的无意义日志,浪费磁盘空间,还干扰排查。

知识体系总览

下面这张图,把本章的测试调试体系串起来了。你可以把它当作一个检查清单,开发完一个功能后,对照着看看哪些环节还没覆盖到。

WebRTC 测试调试体系 单元测试 JUnit + Mockito UI测试 Espresso + IdlingResource 网络模拟测试 ATC / tc 命令 日志系统 RTCStats + 自定义日志 • 信令解析测试 • 网络状态机测试 • 异步回调处理 • Mock PeerConnection • SurfaceView渲染检测 • 按钮交互测试 • 通话状态UI验证 • 横竖屏切换测试 • 带宽限制模拟 • 延迟/抖动模拟 • 丢包场景测试 • 网络切换测试 • WebRTC内部日志 • 关键事件时间戳 • RTCStats指标 • 内存/CPU监控 目标:覆盖所有异常路径,确保通话质量可量化 单元测试锁逻辑 → UI测试验界面 → 网络模拟测极限 → 日志系统留证据

嗯,这套体系搭建起来后,你会发现bug的定位速度快了很多。以前可能要花半天去复现一个偶现问题,现在看看日志里的时间戳和RTCStats曲线,基本就能锁定是哪个环节出了岔子。

最后说一句:测试不是负担,是投资。你在这上面花的时间,会在项目后期十倍百倍地还回来。


公众号:蓝海资料掘金营,微信deep3321