在软件开发中,程序自测是一个永远都绕不开的话题。很多开发人员以写出有难度的代码为荣,但却不重视对自己编写的代码进行测试,这导致了最终到达客户手中的产品质量不高,bug频发,损害了公司的形象。对于一个开发人员来说,我们应该将开发和自测置于同等重要的地位,我们花在自测上的时间要不比开发少。能否对自己编写的代码进行充分的自测也是检验一个开发人员水平高低的标准之一。
自测方法
根据所编写的程序的特点,自测方法大致有如下几种:
第一种,利用模拟工具进行自测。这种方法适用于需要其他模块(尚不具备)发过来的消息才能触发程序流程的情况。模拟工具要严格按照协议的要求发消息,并处理相应的应答消息。这种方法的优点是可模拟真实的系统来测试代码,功能覆盖比较完全;其缺点是模拟工具的编写比较复杂(相当于要实现一个完整的功能模块)、花费的时间较多。
第二种,利用对端模块进行自测。这种方法适用于两个软件模块功能的耦合性比较强且在同时开发的情况。例如,正在开发的模块1的功能A需要正在开发的模块2的功能B才能触发,而模块1和模块2的开发进度差不多,此时,就可以通过模块2向模块1发消息的方法来对功能A和功能B进行自测。
第三种,手动插入数据或执行命令进行自测。这种方法适用于某个软件模块的功能比较独立的情况。此时,没有其他模块与该模块进行消息的交互,也没有编写单独的测试模块。利用手工的方法的优点是可以根据程序的特点设置测试用例,代码覆盖率比较高;其缺点是难以对大数据量的消息进行测试,无法保证程序性能。
第四种,在程序中添加测试代码进行自测。也就是说,在编写软件功能代码的同时,将测试代码一并加入其中。这种方法的优点比较明显,那就是无需与其他程序模块进行消息交互、无需手动插入数据或发消息,当整个软件模块运行起来之后,测试代码即可发挥其功效。当然,该方法也有缺点,那就是编写这些测试代码需要花费一定的时间,可能导致代码过于臃肿。
本文接下来的部分,将用实际的C代码来示例第四种自测方法的使用。
示例程序
本文用到的示例程序比较简单,其功能是每个一段时间(1分钟)将源目录中满足前缀要求的文件备份(移动)到备份目录中。
程序(FilesBackup.c)如下:
/**********************************************************************
* 版权所有(C)2016, Zhou Zhaoxiong。
*
* 文件名称:FilesBackup.c
* 文件标识:无
* 内容摘要:将某个目录中的文件进行备份
* 其它说明:无
* 当前版本:V1.0
* 作 者:Zhou Zhaoxiong
* 完成日期:20160701
*
**********************************************************************/
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <ftw.h>
#include <time.h>
// 重定义数据类型
typedef signed int INT32;
typedef unsigned int UINT32;
typedef unsigned char UINT8;
// 全局变量定义
UINT8 g_szSourceDir[256] = {0}; // 源文件的目录
UINT8 g_szBackupDir[256] = {0}; // 备份文件的目录
// 函数声明
INT32 SelectFlies(struct dirent *pDir);
void ScanDirAndBackup(void);
void Sleep(UINT32 iCountMs);
/****************************************************************
* 功能描述: 主函数
* 输入参数: 无
* 输出参数: 无
* 返回值: 0-执行完成
* 其他说明: 无
* 修改日期 版本号 修改人 修改内容
*-------------------------------------------------------------
* 20160701 V1.0 Zhou Zhaoxiong 创建
****************************************************************/
INT32 main(void)
{
INT32 iRetValue = 0;
// 获取源文件的目录
snprintf(g_szSourceDir, sizeof(g_szSourceDir)-1,"%s/zhouzx/TestDir/SourceDir", getenv("HOME"));
// 获取备份文件的目录
snprintf(g_szBackupDir, sizeof(g_szBackupDir)-1,"%s/zhouzx/TestDir/BackupDir", getenv("HOME"));
// 调用函数执行文件的备份
while (1)
{
ScanDirAndBackup();
Sleep(60 * 1000); // 每一分钟执行一次文件的备份
}
return 0;
}
/**********************************************************************
* 功能描述:根据前缀和后缀选择文件
* 输入参数:dir-目录
* 输出参数:无
* 返回值:0-失败 1-成功
* 其它说明:无
* 修改日期 版本号 修改人 修改内容
*--------------------------------------------------------------------
* 20160701 V1.0 ZhouZhaoxiong 创建
***********************************************************************/
INT32 SelectFlies(struct dirent *pDir)
{
INT32 iSelectResult = 0;
UINT8 szFilePrefix[10] = {0}; // 源文件的前缀
if (pDir == NULL)
{
printf("SelectFlies:input parameter is NULL!\n");
return 0;
}
// 匹配文件前缀和后缀
strncpy(szFilePrefix, "File_", strlen("File_"));
iSelectResult = (strncmp(pDir->d_name, szFilePrefix,strlen(szFilePrefix)) == 0);
if (iSelectResult == 1) // 找到了匹配前缀的文件
{
return 1;
}
else
{
return 0;
}
}
/**********************************************************************
* 功能描述:扫描目录并备份文件
* 输入参数:无
* 输出参数:无
* 返回值:无
* 其它说明:无
* 修改日期 版本号 修改人 修改内容
*--------------------------------------------------------------------
* 20160701 V1.0 ZhouZhaoxiong 创建
***********************************************************************/
void ScanDirAndBackup(void)
{
INT32 iScanDirRet = 0;
UINT32 iFileIdx = 0;
UINT8 szScanedFile[512] = {0};
UINT8 szCmdBuf[256] = {0};
struct dirent **ppDirEnt = NULL;
iScanDirRet = scandir(g_szSourceDir, &ppDirEnt, SelectFlies,alphasort);
if (iScanDirRet < 0) // 扫描目录出错
{
printf("ScanDirAndBackup:exec scandir failed, path=%s\n",g_szSourceDir);
return;
}
else if (iScanDirRet == 0) // 目录下无文件
{
printf("ScanDirAndBackup:no satisfied file in directory %s\n",g_szSourceDir);
return;
}
else // 将满足条件的文件移动到备份目录中
{
for (iFileIdx = 0; iFileIdx < iScanDirRet; iFileIdx ++)
{
memset(szScanedFile, 0x00, sizeof(szScanedFile));
snprintf(szScanedFile, sizeof(szScanedFile) - 1, "%s/%s",g_szSourceDir, ppDirEnt[iFileIdx]->d_name);
memset(szCmdBuf, 0x00, sizeof(szCmdBuf));
snprintf(szCmdBuf, sizeof(szCmdBuf) - 1, "mv %s %s",szScanedFile, g_szBackupDir);
system(szCmdBuf);
printf("ScanDirAndBackup:now, %s\n", szCmdBuf);
}
}
printf("ScanDirAndBackup:this time,totally moved %d file(s) to%s\n", iScanDirRet, g_szBackupDir);
return;
}
/**********************************************************************
* 功能描述:程序休眠
* 输入参数:iCountMs-休眠时间(单位:ms)
* 输出参数:无
* 返回值:无
* 其它说明:无
* 修改日期 版本号 修改人 修改内容
*------------------------------------------------------------------
* 20160701 V1.0 Zhou Zhaoxiong 创建
********************************************************************/
void Sleep(UINT32 iCountMs)
{
struct timeval t_timeout = {0};
if (iCountMs < 1000)
{
t_timeout.tv_sec = 0;
t_timeout.tv_usec = iCountMs * 1000;
}
else
{
t_timeout.tv_sec = iCountMs /1000;
t_timeout.tv_usec = (iCountMs % 1000) * 1000;
}
select(0, NULL, NULL, NULL, &t_timeout); // 调用select函数阻塞程序
}
添加测试代码之后的程序
我们添加测试代码的基本思路是在扫描源文件目录之前,先在该目录下生成文件,这样就相当于手动将文件放到源目录中了。
添加之后的程序代码如下:
/**********************************************************************
* 版权所有(C)2016, Zhou Zhaoxiong。
*
* 文件名称:FilesBackup.c
* 文件标识:无
* 内容摘要:将某个目录中的文件进行备份
* 其它说明:无
* 当前版本:V1.0
* 作 者:Zhou Zhaoxiong
* 完成日期:20160701
*
**********************************************************************/
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <ftw.h>
#include <time.h>
// 重定义数据类型
typedef signed int INT32;
typedef unsigned int UINT32;
typedef unsigned char UINT8;
// 全局变量定义
UINT8 g_szSourceDir[256] = {0}; // 源文件的目录
UINT8 g_szBackupDir[256] = {0}; // 备份文件的目录
// 函数声明
INT32 SelectFlies(struct dirent *pDir);
void ScanDirAndBackup(void);
void Sleep(UINT32 iCountMs);
void CreateTestFile(void);
/****************************************************************
* 功能描述: 主函数
* 输入参数: 无
* 输出参数: 无
* 返回值: 0-执行完成
* 其他说明: 无
* 修改日期 版本号 修改人 修改内容
*-------------------------------------------------------------
* 20160701 V1.0 Zhou Zhaoxiong 创建
****************************************************************/
INT32 main(void)
{
INT32 iRetValue = 0;
// 获取源文件的目录
snprintf(g_szSourceDir, sizeof(g_szSourceDir)-1,"%s/zhouzx/TestDir/SourceDir", getenv("HOME"));
// 获取备份文件的目录 snprintf(g_szBackupDir,sizeof(g_szBackupDir)-1, "%s/zhouzx/TestDir/BackupDir",getenv("HOME"));
// 调用函数执行文件的备份
while (1)
{
// -------------
// 先在源目录中创建测试文件
CreateTestFile();
// -------------
ScanDirAndBackup();
Sleep(60 * 1000); // 每一分钟执行一次文件的备份
}
return 0;
}
/**********************************************************************
* 功能描述:根据前缀和后缀选择文件
* 输入参数:dir-目录
* 输出参数:无
* 返回值:0-失败 1-成功
* 其它说明:无
* 修改日期 版本号 修改人 修改内容
*--------------------------------------------------------------------
* 20160701 V1.0 ZhouZhaoxiong 创建
***********************************************************************/
INT32 SelectFlies(struct dirent *pDir)
{
INT32 iSelectResult = 0;
UINT8 szFilePrefix[10] = {0}; // 源文件的前缀
if (pDir == NULL)
{
printf("SelectFlies:input parameter is NULL!\n");
return 0;
}
// 匹配文件前缀和后缀
strncpy(szFilePrefix, "File_", strlen("File_"));
iSelectResult = (strncmp(pDir->d_name, szFilePrefix,strlen(szFilePrefix)) == 0);
if (iSelectResult == 1) // 找到了匹配前缀的文件
{
return 1;
}
else
{
return 0;
}
}
/**********************************************************************
* 功能描述:扫描目录并备份文件
* 输入参数:无
* 输出参数:无
* 返回值:无
* 其它说明:无
* 修改日期 版本号 修改人 修改内容
*--------------------------------------------------------------------
* 20160701 V1.0 ZhouZhaoxiong 创建
***********************************************************************/
void ScanDirAndBackup(void)
{
INT32 iScanDirRet = 0;
UINT32 iFileIdx = 0;
UINT8 szScanedFile[512] = {0};
UINT8 szCmdBuf[256] = {0};
struct dirent **ppDirEnt = NULL;
iScanDirRet = scandir(g_szSourceDir, &ppDirEnt, SelectFlies,alphasort);
if (iScanDirRet < 0) // 扫描目录出错
{
printf("ScanDirAndBackup:exec scandir failed, path=%s\n",g_szSourceDir);
return;
}
else if (iScanDirRet == 0) // 目录下无文件
{
printf("ScanDirAndBackup:no satisfied file in directory %s\n",g_szSourceDir);
return;
}
else // 将满足条件的文件移动到备份目录中
{
for (iFileIdx = 0; iFileIdx < iScanDirRet; iFileIdx ++)
{
memset(szScanedFile, 0x00, sizeof(szScanedFile));
snprintf(szScanedFile, sizeof(szScanedFile) - 1, "%s/%s", g_szSourceDir,ppDirEnt[iFileIdx]->d_name);
memset(szCmdBuf, 0x00, sizeof(szCmdBuf));
snprintf(szCmdBuf, sizeof(szCmdBuf) - 1, "mv %s %s",szScanedFile, g_szBackupDir);
system(szCmdBuf);
printf("ScanDirAndBackup:now, %s\n", szCmdBuf);
}
}
printf("ScanDirAndBackup:this time,totally moved %d file(s) to%s\n", iScanDirRet, g_szBackupDir);
return;
}
/**********************************************************************
* 功能描述:程序休眠
* 输入参数:iCountMs-休眠时间(单位:ms)
* 输出参数:无
* 返回值:无
* 其它说明:无
* 修改日期 版本号 修改人 修改内容
*------------------------------------------------------------------
* 20160701 V1.0 Zhou Zhaoxiong 创建
********************************************************************/
void Sleep(UINT32 iCountMs)
{
struct timeval t_timeout = {0};
if (iCountMs < 1000)
{
t_timeout.tv_sec = 0;
t_timeout.tv_usec = iCountMs * 1000;
}
else
{
t_timeout.tv_sec = iCountMs /1000;
t_timeout.tv_usec = (iCountMs % 1000) * 1000;
}
select(0, NULL, NULL, NULL, &t_timeout); // 调用select函数阻塞程序
}
/**********************************************************************
* 功能描述:创建本地测试文件
* 输入参数:无
* 输出参数:无
* 返回值:无
* 其它说明:每一轮创建的测试文件数目加1,到达1000个之后又从1开始
* 修改日期 版本号 修改人 修改内容
*--------------------------------------------------------------------
*20160701 V1.0 Zhou Zhaoxiong 创建
**********************************************************************/
void CreateTestFile(void)
{
UINT32 iFileIdx = 0;
UINT8 szFileName[500] = {0};
FILE *fp = NULL;
static UINT32 s_iFileNo = 0;
s_iFileNo ++;
if (s_iFileNo > 1000)
{
s_iFileNo = 0;
}
for (iFileIdx = 0; iFileIdx < s_iFileNo; iFileIdx ++)
{
// 获取带路径的文件名
memset(szFileName, 0x00, sizeof(szFileName));
snprintf(szFileName, sizeof(szFileName)-1, "%s/File_%d.txt",g_szSourceDir, iFileIdx);
fp = fopen(szFileName, "a+");
if (fp == NULL)
{
printf("CreateTestFile: open file %s failed!\n", szFileName);
return;
}
fputs("Hello,world!", fp);
fflush(fp);
fclose(fp);
fp = NULL;
}
if (s_iFileNo % 10 == 0) // 每生成10批满足前缀要求的文件之后, 生成1个不满足前缀要求的文件
{
memset(szFileName, 0x00, sizeof(szFileName));
snprintf(szFileName, sizeof(szFileName)-1, "%s/Test_%d.txt",g_szSourceDir, s_iFileNo);
fp = fopen(szFileName, "a+");
if (fp == NULL)
{
printf("CreateTestFile: open file %s failed!\n", szFileName);
return;
}
fputs("Hello,world!", fp);
fflush(fp);
fclose(fp);
fp = NULL;
}
}
程序说明如下:
第一,本程序中添加的测试函数是CreateTestFile,其作用是在源目录中创建测试文件。考虑到程序性能,我们设定最多生成1000(可根据实际情况修改)个满足前缀要求的文件,并且每一轮生成的文件数比上一轮多一个。同时,每生成10轮的满足前缀要求的文件之后,要生成一个不满足前缀要求的文件,用以测试异常情况。如此,正常和异常情况都考虑到了。
第二,示例代码中写入文件的内容是固定的“Hello,world!”,大家可以根据需要修改该内容以满足自身测试的要求。
第三,在提交正式版本的时候,大家要将测试代码注释掉或删掉,以免影响正常的程序流程。
总结
很多人所理解的软件开发人员的工作就是写代码,而不包括测试,这样的理解是片面的。实际的经验表明,很多时候,我们花在测试上的时间比写代码的时间还要多。为了保证产品质量,很多项目组也对自测提出了较高的要求。
作为一位合格的软件开发人员,自测是一个检验和提升自身能力的好方法,大家一定要对自己编写的代码进行充分的测试。通过不断地实践,大家也可以总结出更多和更好的自测方法。