原文链接:

让你提前认识软件开发(19):C语言中的协议及单元测试示例

CSDN博客 https://blog.csdn.net/zhouzhaoxiong1227/article/details/25242311


Contents:

一、软件模块之间的协议

二、单元测试

三、本程序中的协议

四、代码

五、单元测试用例

六、总结


【文章摘要】

在实际的软件开发项目中,经常要实现多个模块之间的通信,这就需要大家约定好相互之间的通信协议,各自按照协议来收发和解析消息。

本文以实际的程序代码为例,详细介绍了如何用C语言来实现通信协议,并基于对协议字段的判断,说明了程序单元测试的过程,为相关的开发工作提供了有益的参考。

【关键词】

软件开发  协议  单元测试  C语言  字段

一、软件模块之间的协议

什么是软件模块之间的协议?不同的软件模块之间要实现相互通信,就必须遵循共同的消息规范,大家按照约定好的规范来收发消息。软件模块之间的协议就是不同模块间消息交互的规范。

在通信协议中,一条完整的消息由消息头和消息体构成,如图1所示。

图1 一条完整的消息示意图

在C语言中,用结构体来表示协议。在进行消息解析的时候,一般只关注消息体的内容。消息头只是用于标识一条消息,让其它模块能够识别该类消息。

二、单元测试

在提交程序版本之前,开发人员需要对代码进行单元测试和集成测试。那么什么是单元测试呢?单元测试就是对程序中的一个函数进行测试,看对于某个输入,是否有预期的输出。

单元测试的示意图如图2所示。

图2 单元测试的示意图

可以把函数看成一个灰色的盒子,测试的时候只关心输入和输出,要设计多组单元测试数据来对函数的功能进行测试。

此外,在测试中,还有一个叫做“测试用例”的概念。测试用例就是一次测试的整个过程,包括:测试目的、预置条件、测试步骤、预期结果、通过准则、测试工具等。

三、本程序中的协议

本程序中的协议包括了消息头和消息体,其中,消息头有四个字段,消息体有五个字段。如下代码所示。

// 消息头结构
typedef struct
{
UINT16 iReserve1;
UINT16 iReserve2;
UINT16 iReserve3;
UINT16 iReserve4;
}MsgHead_T; // 消息结构体(包含消息头和消息体)
typedef struct
{
MsgHead_T MsgHead; // 消息头
UINT32 iOperType; // 操作类型
UINT8 szUserNumber[]; // 用户号码
UINT8 szOperTime[]; // 操作时间, 格式为: yyyymmdd
UINT32 iReserve1; // 保留字段1
UINT8 szReserve2[]; // 保留字段2
}UserReqMsg_T;

在消息体的五个字段中,操作类型、用户号码和操作时间是本次要进行判断处理的字段,另外两个字段是保留字段,可以先不用赋具体的值。

在协议中,为什么要留有保留字段呢?这是方便以后对协议进行扩展。也就是说,如果以后除了操作类型、用户号码和操作时间之外,还需要增加新的字段定义,可以直接利用扩展字段。这在实际的软件开发项目中是很重要的。

四、程序代码

基于以上协议,本文中的程序代码如下所示:

/**********************************************************************
* 版权所有 (C)2014, Zhou Zhaoxiong。
* 文件名称: UnitTest.c
* 内容摘要:协议及单元测试示例代码
**********************************************************************/ #include <stdio.h>
#include <string.h> // 重定义数据类型
typedef unsigned char UINT8;
typedef unsigned short int UINT16;
typedef unsigned int UINT32;
typedef signed int INT32; // 消息头结构
typedef struct
{
UINT16 iReserve1;
UINT16 iReserve2;
UINT16 iReserve3;
UINT16 iReserve4;
}MsgHead_T; // 消息结构体(包含消息头和消息体)
typedef struct
{
MsgHead_T MsgHead; // 消息头
UINT32 iOperType; // 操作类型, 操作类型只能为1或2
UINT8 szUserNumber[]; // 用户号码
UINT8 szOperTime[]; // 操作时间, 格式为: yyyymmdd
UINT32 iReserve1; // 保留字段1
UINT8 szReserve2[]; // 保留字段2
}UserReqMsg_T; // 函数声明
INT32 ProcUserReqMsg(UserReqMsg_T *ptUserReqMsg);
INT32 main(); /**********************************************************************
* 功能描述:主函数
* 输入参数:无
* 输出参数:无
* 返回值: 0-执行完毕
***********************************************************************/ INT32 main()
{
UINT8 iRetVal = ;
UINT32 iOperType = ; // 操作类型
UINT8 szUserNumber[] = {}; // 用户号码
UINT8 szOperTime[] = {}; // 操作时间, 格式为: yyyymmdd UserReqMsg_T tUserReqMsg = {}; // 请求消息

// 对消息头部进行赋值
tUserReqMsg.MsgHead.iReserve1 = ;
tUserReqMsg.MsgHead.iReserve2 = ;
tUserReqMsg.MsgHead.iReserve3 = ;
tUserReqMsg.MsgHead.iReserve4 = ; // 读入具体消息字段的值
printf("操作类型: \n");
scanf("%d", &iOperType);
printf("用户号码: \n");
scanf("%s", szUserNumber);
printf("操作时间: \n");
scanf("%s", szOperTime); // 对具体消息字段进行赋值(保留字段可不赋值)
tUserReqMsg.iOperType = iOperType;
strncpy(tUserReqMsg.szUserNumber, szUserNumber, strlen(szUserNumber));// 获取号码, 用strncpy代替strcpy
strncpy(tUserReqMsg.szOperTime, szOperTime, strlen(szOperTime)); // 获取时间, 用strncpy代替strcpy // 对消息体的字段进行异常判断
iRetVal = ProcUserReqMsg(&tUserReqMsg); // 注意: 传递参数的时候要加上&
if (iRetVal == ) // 函数执行正确
{
// 打印消息字段内容
printf("The user request message is: iOperType=%d, szUserNumber=%s, szOperTime=%s.\n", tUserReqMsg.iOperType, tUserReqMsg.szUserNumber, tUserReqMsg.szOperTime);
return ;
}
else // 打印异常消息
{
printf("Some content of the user request message is wrong, please check!\n");
return -;
}
} /**********************************************************************
* 功能描述:对消息体的字段进行异常判断
* 输入参数: ptUserReqMsg-用户请求消息
* 输出参数:无
* 返回值: 0-成功 其它-失败
* 其它说明:无
***********************************************************************/
INT32 ProcUserReqMsg(UserReqMsg_T *ptUserReqMsg)
{
INT32 iRetValue = ; // 对输入参数进行异常判断
if (ptUserReqMsg == NULL)
{
printf("ProcUserReqMsg(...): input parameter(ptUserReqMsg) is NULL.\n");
return -;
} // 对消息体字段进行异常判断
if ((ptUserReqMsg->iOperType != ) && (ptUserReqMsg->iOperType != )) // 操作类型只能为1或2, 其它为数据异常
{
printf("ProcUserReqMsg(...): the iOperType is wrong, iOperType=%d.\n", ptUserReqMsg->iOperType);
return -;
} if (strlen(ptUserReqMsg->szUserNumber) != ) // 用户号码异常, 长度8位才正确
{
printf("ProcUserReqMsg(...): the szUserNumber is wrong.\n");
return -;
} if (strlen(ptUserReqMsg->szOperTime) != ) // 操作时间异常, 长度8位才正确
{
printf("ProcUserReqMsg(...): the szOperTime is wrong.\n");
return -;
} return ;
}

本程序要对ProcUserReqMsg函数进行单元测试,看该函数能否对消息体的字段进行异常判断。

五、单元测试用例

1. 正常测试用例

正常测试用例是指满足程序输入条件的测试用例,即观察程序在正确的输入情况下,能否产生正确的输出。

什么是正常测试?包括了两种情况:
        1) 输入正确的值,程序产生正确的输出。
        2) 输入错误的值,程序产生错误的输出。

(1) “操作类型”为1

设定“操作类型”为1,“用户号码”和“操作时间”字段均符合协议要求。程序的执行情况如图3所示。

图3 “操作类型”为1的正常执行情况

(2) “操作类型”为2

设定“操作类型”为2,“用户号码”和“操作时间”字段均符合协议要求。程序的执行情况如图4所示。

图4 “操作类型”为2的正常执行情况

2. 异常测试用例

异常测试用例是指不满足程序输入条件的测试用例,即观察程序在错误的输入情况下,产生的结果是怎样的。

什么是异常测试?包括了两种情况:
        1) 输入正确的值,程序产生错误的输出。
        2) 输入错误的值,程序产生正确的输出。

(1) “操作类型”不为1或2

设定“操作类型”为3(不为1或2的正整数),“用户号码”和“操作时间”字段均符合协议要求。程序的执行情况如图5所示。

图5 “操作类型”为3的异常执行情况

(2) “用户号码”不是8位

设定“操作类型”为1,“用户号码”字段为9位,“操作时间”字段符合协议要求。程序的执行情况如图6所示。

图6 “用户号码”为9位的异常执行情况

(3) “操作时间”不是8位

设定“操作类型”为1,“用户号码”符合协议要求,“操作时间”字段为9位。程序的执行情况如图7所示。

图7 “操作时间”为9位的异常执行情况

正常和异常测试的情况都有很多种,这里就不一一列举了。为了确保程序的正确性,一定要对程序(或者函数)进行充分的单元测试。

六、总结

对于协议,这是不同模块之间通信的桥梁。因此,在开始编码之前,一定要将协议定义清楚,这样也可以减少后续修改带来的不便。

对于单元测试,这是每个软件开发工程师都必须要认真对待的。单元测试进行得是否彻底,会直接影响到软件产品的质量。

本文以实际的程序代码为例子,对用C语言表示协议和对代码进行单元测试作了详细的介绍。文中涉及到的协议表示方法和单元测试方法可供相关的软件开发工程师参考。


【阅读笔记】《C程序员 从校园到职场》第八章 算法和协议(Part 2)的更多相关文章

  1. 【阅读笔记】《C程序员 从校园到职场》第三章 程序的样式(大括号)

    参考: https://blog.csdn.net/zhouzhaoxiong1227/article/details/22820533 一..初始化数组变量 在实际的软件开发项目中,变量在使用前应初 ...

  2. 【阅读笔记】《C程序员 从校园到职场》第七章 指针和结构体

    原文地址:让你提前认识软件开发(13):指针及结构体的使用 CSDN博客 https://blog.csdn.net/zhouzhaoxiong1227/article/details/2387299 ...

  3. 【阅读笔记】《C程序员 从校园到职场》第六章 配置文件,makefile 文件 (Part 2)

     Contents: 1.配置文件(通常以 ini 结尾) 2.makefile文件 (Linux) PS: 这篇文章的内容,不太理解. 一.配置文件 本文以一个实际的小软件为例,介绍了C语言中配置文 ...

  4. 【阅读笔记】《C程序员 从校园到职场》第五章 内存操作

    参考:   让你提前认识软件开发(8):memset()与memcpy()函数  https://blog.csdn.net/zhouzxi/article/details/22478081 让你提前 ...

  5. 【阅读笔记】《C程序员 从校园到职场》第二章 学校到职场

    一.代码规范: 1.变量命名(让人一眼看它是什么意思,要做什么操作),定义并初始化 2.函数命名规范(函数的功能)在主函数之前进行声明. 在实际项目中,一般不在函数调用者的内部来对被调函数进行声明,而 ...

  6. 【阅读笔记】《C程序员 从校园到职场》第六章 常用文件操作函数 (Part 1)

    参考链接:https://blog.csdn.net/zhouzhaoxiong1227/article/details/24926023 让你提前认识软件开发(18):C语言中常用的文件操作函数总结 ...

  7. 【阅读笔记】《C程序员 从校园到职场》第四章 变量和函数

    参考: Contents: 一.数据类型(对基本数据类型进行重定义——规范化) 二.变量和函数  (命名规则,注意事项) 三.静态变量及其使用 一.数据类型(对基本数据类型进行重定义——规范化) 1. ...

  8. 《C程序猿从校园到职场》带领大家从校园走向职场

    七夕节刚过.就有好消息传来:本人新书<C程序猿从校园到职场>正式出版并在各大电商平台上发售了! 以下.让我们一起来赞赏一下纸质书的"风採"吧. 本书文件夹 第1章 概述 ...

  9. 《C程序猿从校园到职场》勘误

    (本人正在參加2015博客之星评选.诚邀你来投票,谢谢:username=zhouzxi">http://vote.blog.csdn.net/blogstar2015/candida ...

随机推荐

  1. react native出现 undefined is not a function_this4.错误函数无法识别

    该函数可能里可能有this,的上个函数this要绑定bind(this)

  2. 面向对象编程之OC

    面向对象概述 面向对象是一种符合人类思想习惯的编程思想.现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系,在程序中使用对象来映射现实中的事物,使用对象的关系来描述事物之间的联系,这种 ...

  3. word之常用功能

    0.word区域:标题栏.快速访问工具栏.功能区.功能按钮.导航窗口.编辑区.水平垂直滑动条.状态栏 1.更改office主题.文件---帐户---office主题.(传统白色.浅灰色.深灰色) 2. ...

  4. xxnet to google部署

    1,github上下载xxnet项目 2,启动(点击 start) 3,确定启动好后访问 www.google.com (此时是可以访问的) 4,注册google账号或直接登陆 5,访问 https: ...

  5. Java面试题整理---JVM篇

    1.JVM运行时内存区域划分?   2.内存溢出OOM和堆栈溢出SOE的案例.原因.排查及解决?   3.常用的JVM性能监控工具?   4.JVM参数设置?   5.类加载过程?   6.JVM内存 ...

  6. MongoDB运维心得(一)

    问题:集群内部通信压力较大.出现在某一个节点创建普通表并插入数据,在其他点读的问题.会造成每次读表都要进行一次完整的数据传输. 前提: Mongodb处于Sharding Cluster状态. 造成原 ...

  7. swapper_pg_dir主内核页表、init和kthreadd、do_fork时新建子进程页表、vmalloc与kmalloc

    都是以前看到一个点扯出的很多东西,当时做的总结,有问题欢迎讨论,现在来源难寻,侵删! 1.Init_task.idle.init和kthreadd的区别和联系 idle进程其pid=0,其前身是系统创 ...

  8. Linux下系统时间函数、DST等相关问题总结(转)

    Linux下系统时间函数.DST等相关问题总结 下面这个结构体存储了跟时区相关的位移量(offset)以及是否存在DST等信息,根据所在的时区信息,很容易找到系统时间与UTC时间之间的时区偏移,另外根 ...

  9. nginx-相关功能分析 第四章

    # Nginx服务器的rewrite.全局变量.重定向和防盗链相关功能 一:Nginx 后端服务器组的配置: 1.upstream: 用于定义可由proxy_pass,fastcgi_pass,uws ...

  10. CSS 使用技巧

    CSS 使用技巧 1.CSS代码重用,解决同一类样式下相同冲突点 <style> .c { 共有 } .c1 { 独有 } .c2 { 独有 } </style> <di ...