测试文件I/O:模拟文件系统、使用临时文件进行读写测试
文件I/O测试,说实话,是很多嵌入式工程师容易忽视的一块。大家总觉得读写文件嘛,跑一遍不就知道了?但我在项目中吃过不少亏——有一次,一个文件写入的bug在开发环境死活复现不了,到了客户现场,文件系统一满,程序直接崩溃。从那以后,我对待文件I/O测试的态度就变了。
今天咱们聊聊怎么系统地测文件I/O。核心思路就两条:要么模拟文件系统,要么用临时文件。两条路各有各的玩法。
为什么文件I/O测试这么特殊?
文件I/O测试的难点在于——它依赖外部环境。你想想看,测试代码跑在开发机上,文件系统是PC的;但程序最终跑在嵌入式设备上,文件系统可能是FAT32、LittleFS或者SPI Flash上的裸分区。环境不同,行为就可能不同。
我个人的习惯是:能模拟就模拟,不能模拟就用临时文件隔离。这样测试既快又可靠,不会因为磁盘满了或者权限问题导致测试失败。
方案一:模拟文件系统
模拟文件系统,说白了就是自己写一套假的文件操作接口。把fopen、fread、fwrite这些函数替换成内存操作。这样测试跑起来飞快,而且完全可控。
来看一个简单的例子。假设我们有这样一个文件读写函数:
// 原始代码:从文件读取配置
int read_config(const char *path, Config *cfg) {
FILE *fp = fopen(path, "rb");
if (!fp) return -1;
size_t n = fread(cfg, sizeof(Config), 1, fp);
fclose(fp);
return (n == 1) ? 0 : -1;
}
要测试它,我们可以这样模拟文件系统:
// 测试代码:模拟文件系统
#include <setjmp.h>
#include <stdarg.h>
#include <stddef.h>
#include <cmocka.h>
// 模拟的fopen:从内存缓冲区读取
static FILE *mock_fopen(const char *path, const char *mode) {
// 检查路径是否合法
check_expected(path);
check_expected(mode);
// 返回一个模拟的文件指针
return (FILE *)mock();
}
// 模拟的fread:从内存拷贝数据
static size_t mock_fread(void *ptr, size_t size, size_t nmemb, FILE *stream) {
check_expected(ptr);
check_expected(size);
check_expected(nmemb);
// 从mock数据中拷贝
void *data = (void *)mock();
size_t data_size = (size_t)mock();
size_t to_copy = (data_size < size * nmemb) ? data_size : size * nmemb;
memcpy(ptr, data, to_copy);
return to_copy / size;
}
// 测试用例
static void test_read_config_success(void **state) {
Config expected = {.version = 1, .baudrate = 115200};
// 设置模拟行为
will_return(mock_fopen, (FILE *)0x1234); // 返回非NULL指针
expect_string(mock_fopen, path, "/cfg.bin");
expect_string(mock_fopen, mode, "rb");
will_return(mock_fread, &expected);
will_return(mock_fread, sizeof(Config));
Config actual;
int ret = read_config("/cfg.bin", &actual);
assert_int_equal(ret, 0);
assert_int_equal(actual.version, 1);
assert_int_equal(actual.baudrate, 115200);
}
核心思路:用mock替换真实的文件操作,让测试在内存中完成。这样不需要真实的文件,测试速度极快,而且可以模拟各种边界情况——比如文件不存在、读取失败、数据损坏等。
方案二:使用临时文件
有些场景下,模拟文件系统太麻烦。比如你要测试文件系统的碎片整理、日志轮转、或者跨进程文件共享。这时候,用临时文件更实在。
我在项目中常用的做法是:每个测试用例创建独立的临时文件,测试结束后自动清理。这样测试之间不会互相干扰。
看一个用C标准库实现的例子:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
// 辅助函数:创建临时文件并写入数据
static FILE *create_temp_file(const char *content) {
// 创建临时文件
char template[] = "/tmp/test_XXXXXX";
int fd = mkstemp(template);
if (fd == -1) return NULL;
FILE *fp = fdopen(fd, "w+");
if (!fp) {
close(fd);
return NULL;
}
// 写入内容
if (content) {
fwrite(content, 1, strlen(content), fp);
rewind(fp);
}
return fp;
}
// 测试用例:读取文件内容
static void test_read_file_content(void) {
const char *test_data = "Hello, File I/O Test!";
FILE *fp = create_temp_file(test_data);
assert_non_null(fp);
char buffer[64] = {0};
size_t n = fread(buffer, 1, sizeof(buffer) - 1, fp);
buffer[n] = '\0';
assert_string_equal(buffer, test_data);
fclose(fp);
// 注意:这里需要删除临时文件
// 实际项目中可以用atexit注册清理函数
}
我的经验:临时文件的路径最好用环境变量控制。比如设置TMPDIR环境变量,这样在CI环境里可以统一管理。我曾经遇到过测试在开发机上跑得好好的,上了CI就失败——就是因为临时文件路径写死了。
两种方案的对比
| 维度 | 模拟文件系统 | 临时文件 |
|---|---|---|
| 测试速度 | 极快(纯内存操作) | 较慢(涉及磁盘I/O) |
| 环境依赖 | 无(完全隔离) | 需要文件系统支持 |
| 边界测试 | 容易模拟各种异常 | 需要构造真实场景 |
| 代码侵入性 | 需要设计可mock的接口 | 无侵入 |
| 适用场景 | 单元测试、逻辑验证 | 集成测试、系统测试 |
避坑指南:我踩过的几个坑
坑一:忘记清理临时文件
我曾经有一个测试套件,每次跑完留下一堆临时文件。一个月后,磁盘满了,整个CI构建都挂了。教训是:一定要用atexit或者RAII机制确保清理。在C语言里,我习惯这样写:
// 注册清理函数
static char temp_files[10][256];
static int temp_file_count = 0;
void cleanup_temp_files(void) {
for (int i = 0; i < temp_file_count; i++) {
remove(temp_files[i]);
}
}
// 在main函数开始注册
int main() {
atexit(cleanup_temp_files);
// ... 测试逻辑
}
坑二:文件名冲突
多线程测试时,多个线程同时创建临时文件,文件名可能冲突。解决方案是用mkstemp或者tmpfile,它们能保证唯一性。我自己写测试时,永远不用硬编码的文件名。
坑三:权限问题
在嵌入式环境里,文件系统可能是只读的。测试时如果没考虑到这一点,代码在开发机上跑得好好的,部署到设备上就挂了。我的建议是:在测试中至少有一个用例验证只读文件系统的行为。
知识体系总览
下面这张图总结了文件I/O测试的核心思路和决策流程:
实战建议
说了这么多,最后给几条实在的建议:
- 单元测试阶段:优先用模拟文件系统。速度快,可控性强,能覆盖各种边界情况。
- 集成测试阶段:用临时文件。这时候需要验证真实的文件系统交互。
- 永远不要:在测试中依赖特定的文件路径或文件内容。测试应该是自包含的。
- 记得测试异常路径:文件不存在、权限不足、磁盘满、文件被锁定——这些场景在真实环境中都会遇到。
特别提醒:如果你在嵌入式环境下做测试,一定要注意文件系统的差异。比如FAT32不支持符号链接,LittleFS的写入次数有限。我见过一个团队,在Linux上测试通过了,部署到RTOS上文件写入就失败——因为RTOS的文件系统不支持同时打开多个文件句柄。
文件I/O测试,说白了就是让你的代码在各种文件系统环境下都能稳定工作。掌握了模拟和临时文件这两把刷子,大部分场景都能覆盖。嗯,今天就聊到这儿,下次咱们聊聊网络通信的测试——那又是另一番天地了。
公众号:蓝海资料掘金营,微信deep3321