20、测试网络通信:Mock socket API、测试TCP/UDP协议处理逻辑
网络通信这块,说实话,是嵌入式开发里最让人头疼的测试场景之一。你想想看,你的代码要跟远端服务器握手、收发数据包,但测试环境里哪有真正的网络?总不能每次跑单元测试都去连一台真实的服务器吧?
我早年做物联网网关项目时,就吃过这个亏。当时写了一个TCP重传逻辑,自测时一切正常,结果一上真实网络,丢包率一高,重传机制直接崩了。后来复盘发现,问题就出在——我从来没在单元测试里模拟过「网络异常」这个场景。
所以,今天我们就来聊聊:如何用Mock技术,把socket API给“骗”过去,让单元测试能独立、可控地验证TCP/UDP协议处理逻辑。
为什么需要Mock socket API?
直接调用真实的socket()、send()、recv(),有几个硬伤:
- 环境依赖:测试必须跑在有网络、有特定端口的机器上。
- 不可控:你没法让网络在特定时刻“恰好”丢一个包。
- 速度慢:一次真实的TCP握手要几十毫秒,跑几千个用例得等到猴年马月。
- 副作用:测试可能真的把数据发到生产环境去了——别笑,我见过有人干过这事。
说白了,我们要的是确定性。Mock掉socket层,让被测代码以为自己在跟网络通信,实际上是在跟一个模拟的“假网络”交互。这样,丢包、超时、乱序、连接断开……所有你想测的异常场景,都能在毫秒级复现。
Mock策略:函数指针 vs 链接替换
在C语言里Mock socket API,主流做法有两种。我个人更推荐第二种,但先都说说。
1. 函数指针(运行时替换)
把send()、recv()等函数声明为函数指针,测试时指向Mock实现。优点是灵活,缺点是侵入性强——你得改原有代码的结构。
2. 链接时替换(Link-time substitution)
这是我最常用的方式。编译时,把真实的socket库链接换成我们自己写的Mock库。被测代码一行不改,链接器自动把send()调用指向我们的Mock函数。
核心思路:写一个mock_socket.c,里面实现socket()、bind()、listen()、accept()、send()、recv()等函数的“假版本”。测试时,链接这个文件而不是真实的libsocket。
实战:Mock一个简单的TCP客户端
假设我们有这样一个TCP发送函数:
// tcp_client.c
int tcp_send_data(const char *host, int port, const uint8_t *data, size_t len) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) return -1;
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
inet_pton(AF_INET, host, &addr.sin_addr);
if (connect(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
close(sock);
return -2;
}
ssize_t sent = send(sock, data, len, 0);
close(sock);
return (sent == len) ? 0 : -3;
}
我们要测试这个函数,但不想真的去连接服务器。写一个Mock文件:
// mock_socket.c
#include <stdint.h>
#include <string.h>
#include <errno.h>
// 模拟socket描述符
static int mock_sock_fd = 100;
static int connect_result = 0; // 0表示成功
static int send_result = 0; // 0表示成功
static ssize_t send_bytes = 0; // 实际发送的字节数
// 测试辅助函数:设置connect失败
void mock_set_connect_fail(int fail) {
connect_result = fail ? -1 : 0;
}
// 测试辅助函数:设置send失败
void mock_set_send_fail(int fail) {
send_result = fail ? -1 : 0;
}
// Mock实现
int socket(int domain, int type, int protocol) {
// 返回一个假的fd
return mock_sock_fd;
}
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) {
if (connect_result != 0) {
errno = ECONNREFUSED;
return -1;
}
return 0;
}
ssize_t send(int sockfd, const void *buf, size_t len, int flags) {
if (send_result != 0) {
errno = EIO;
return -1;
}
// 记录发送的数据,方便后续断言
send_bytes = len;
return len;
}
int close(int fd) {
return 0;
}
测试用例就可以这样写:
// test_tcp_client.c
#include "unity.h"
#include "tcp_client.h"
void setUp(void) {
// 每次测试前重置Mock状态
mock_set_connect_fail(0);
mock_set_send_fail(0);
}
void test_tcp_send_data_success(void) {
uint8_t data[] = "Hello";
int ret = tcp_send_data("192.168.1.1", 8080, data, 5);
TEST_ASSERT_EQUAL(0, ret);
}
void test_tcp_send_data_connect_fail(void) {
mock_set_connect_fail(1);
uint8_t data[] = "Hello";
int ret = tcp_send_data("192.168.1.1", 8080, data, 5);
TEST_ASSERT_EQUAL(-2, ret); // 预期返回连接失败码
}
void test_tcp_send_data_send_fail(void) {
mock_set_send_fail(1);
uint8_t data[] = "Hello";
int ret = tcp_send_data("192.168.1.1", 8080, data, 5);
TEST_ASSERT_EQUAL(-3, ret); // 预期返回发送失败码
}
小技巧:我习惯在Mock里加一个“调用计数器”。比如mock_get_send_call_count(),用来验证被测代码是否真的调用了send(),以及调用了多少次。这在测试重传逻辑时特别有用。
UDP协议处理的Mock差异
UDP和TCP的Mock思路一样,但有几个细节要注意:
- UDP没有
connect(),所以Mock里不需要模拟连接状态。 - UDP的
sendto()和recvfrom()需要额外处理目标地址参数。 - UDP的“丢包”模拟更简单——直接让
sendto()返回成功但recvfrom()不返回数据即可。
我曾经在一个项目中,需要测试UDP的“心跳保活”机制。心跳包每5秒发一次,如果连续3次没收到回应就切换备用通道。用Mock模拟丢包,我只需要让recvfrom()在前3次调用时返回-1(超时),第4次才返回正常数据。整个测试跑下来不到100毫秒,而如果真用网络测,至少得等15秒。
SVG:Mock socket测试架构图
避坑指南:我曾经踩过的三个坑
坑一:忘记重置Mock状态
我曾经有一次,测试A用例设置了mock_set_connect_fail(1),但测试B用例的setUp()里忘了重置。结果B用例莫名其妙地失败了,排查了半小时才发现是Mock状态污染。所以,每个测试用例的setUp()里,一定要把所有Mock状态恢复到默认值。
坑二:Mock函数签名不一致
有一次我Mock的send()函数,参数flags的类型写成了int,但真实系统里是unsigned int。链接时没报错,但运行时传参错位,数据全乱了。我的教训是:从系统头文件里把函数声明原封不动复制过来,不要手打。
坑三:忽略了errno的模拟
很多网络错误码是通过errno传递的。如果你的被测代码会检查errno(比如EAGAIN、ECONNRESET),那Mock里必须正确设置errno。我见过有人Mock的recv()返回-1但不设置errno,结果被测代码读到一个垃圾值,行为完全不可预测。
总结一下
Mock socket API的核心,就是用可控的假实现替换不可控的真网络。链接时替换是最干净的方式,不污染被测代码。UDP和TCP的Mock思路一致,但要注意UDP没有连接态,丢包模拟更直接。
嗯,最后说一句:别觉得Mock是“作弊”。恰恰相反,能把网络层Mock好,说明你对协议的理解足够深——你知道哪些行为是协议规定的,哪些是网络环境带来的。这才是真正的测试功底。
公众号:蓝海资料掘金营,微信deep3321