关于使用Visual Studio进行C++单元测试

关于使用Visual Studio进行C++单元测试

什么是单元测试?单元测试(unit testing)在软件开发领域有着悠久的历史。在大多数有关单元测试的观念中都有这么一个共同的理念,即它们由一组独立的测试构成,其中每个测试针对一个单独的软件组件。在过程式程序设计的代码中,“单元”一般来说指的就是函数,而在面向对象的代码中则指的是类。

而单元测试则做到了大型测试所不能做到的那些事情。利用单元测试可以独立地对某一段代码进行测试。我们可以将测试分组以便在某些特定条件下运行某些特定的测试,并在其他条件下运行另一些测试。我们还可以迅速定位错误。如果认为在某段代码中存在着一个错误而且又可以在测试用具中使用这段代码的话,我们通常能够迅速地编写出一段测试,看看我们所推测的错误是不是真的在那里。

(一)关于单元测试的一些流行误解(反面即为单元测试的好处)

1. 单元测试是在浪费时间

一旦编码完成, 开发人员总是会迫切希望进行软件的集成工作,这样他们就能够看到实际的系统开始启动工作了。 这在外表上看来是一项明显的进步,而象单元测试这样的活动也许会被看作是通往这个阶段点的道路上的障碍, 推迟了对整个系统进行联调这种真正有意思的工作启动的时间。

在这种开发步骤中, 真实意义上的进步被外表上的进步取代了。 系统能够正常工作的可能性是很小的, 更多的情况是充满了各式各样的Bug。在实践中,这样一种开发步骤常常会导致这样的结果:软件甚至无法运行。 

更进一步的结果是大量的时间将被花费在跟踪那些包含在独立单元里的简单的Bug上面,在个别情况下,这些Bug也许是琐碎和微不足道的,但是总的来说,他们会导致在软件集成为一个系统时增加额外的工期,而且当这个系统投入使用时也无法确保它能够可靠运行。

在实践工作中,进行了完整计划的单元测试和编写实际的代码所花费的精力大致上是相同的。一旦完成了这些单元测试工作,很多Bug将被纠正,在确信他们手头拥有稳定可靠的部件的情况下,开发人员能够进行更高效的系统集成工作。这才是真实意义上的进步,所以说完整计划下的单元测试是对时间的更高效的利用。而调试人员的不受控和散漫的工作方式只会花费更多的时间而取得很少的好处。

点评:如果产品的规模非常小(例如小工具),并且产品的质量不重要(或无所谓),那么“单元测试是在浪费时间”这样的说法在某种程序上是正确的。

如果产品质量非常重要,那么就意味着即使你不在单元测试阶段投入时间发现并修改bug,存在的bug也会在后续的阶段被发现,需要说明的是bug发现得越晚,修改它所付出的代价越大。这样违背原则:尽早发现并修改bug,以减少软件开发成本。

2. 单元测试仅仅是证明这些代码做了什么

这是那些没有首先为每个单元编写一个详细的规格说明而直接跳到编码阶段的开发人员提出的一条普遍的抱怨,当编码完成以后并且面临代码测试任务的时候,他们就阅读这些代码并找出它实际上做了什么,把他们的测试工作基于已经写好的代码的基础上。当然,他们无法证明任何事情。所有的这些测试工作能够表明的事情就是编译器工作正常。是的,他们也许能够抓住(希望能够)罕见的编译器Bug,但是他们能够做的仅仅是这些。如果他们首先写好一个详细的规格说明,测试能够以规格说明为基础。代码就能够针对它的规格说明,而不是针对自身进行测试。这样的测试仍然能够抓住编译器的Bug,同时也能找到更多的编码错误,甚至是一些规格说明中的错误。 好的规格说明可以使测试的质量更高,所以最后的结论是高质量的测试需要高质量的规格说明。在实践中会出现这样的情况:一个开发人员要面对测试一个单元时只给出单元的代码而没有规格说明这样吃力不讨好的任务。你怎样做才会有更多的收获,而不仅仅是发现编译器的Bug?第一步是理解这个单元原本要做什么,不是它实际上做了什么。比较有效的方法是倒推出一个概要的规格说明。这个过程的主要输入条件是要阅读那些程序代码和注释,主要针对这个单元,及调用它和被它调用的相关代码。画出流程图是非常有帮助的,你可以用手工或使用某种工具。可以组织对这个概要规格说明的走读(Review),以确保对这个单元的说明没有基本的错误,有了这种最小程度的代码深层说明,就可以用它来设计单元测试了。

点评:合理的单元测试不仅可以保证单元代码正确实现,还可以在编码过程中用来调试单元代码、以及随时了解单元测试覆盖了多少单元代码。

3. 我是个很棒的程序员, 我是不是可以不进行单元测试?

在每个开发组织中都至少有一个这样的开发人员,他非常擅长于编程,他们开发的软件总是在第一时间就可以正常运行,因此不需要进行测试。你是否经常听到这样的借口?

在真实世界里,每个人都会犯错误。即使某个开发人员可以抱着这种态度在很少的一些简单的程序中应付过去。但真正的软件系统是非常复杂的。真正的软件系统不可以寄希望于没有进行广泛的测试和Bug修改过程就可以正常工作。

编码不是一个可以一次性通过的过程。在真实世界中,软件产品必须进行维护以对操作需求的改变作出反应,并且要对最初的开发工作遗留下来的Bug进行修改。你希望依靠那些原始作者进行修改吗?这些制造出这些未经测试的原始代码的资深专家们还会继续在其他地方制造这样的代码。在开发人员做出修改后进行可重复的单元测试可以避免产生那些令人不快的负作用。

点评:我承认你是一个很棒的程序员,你很少犯错误。但很少犯错误不等于不犯错误,并且你的代码一定有其他人维护的时候(比如你离职了)。试问一下,当其他人修改你写的代码的时候,怎样才能保证他修改后的正确性呢?因此,答案是显而易见地。

4. 不管怎样,集成测试将会抓住所有的Bug

我们已经在前面的讨论中从一个侧面对这个问题进行了部分的阐述。这个论点不成立的原因在于规模越大的代码集成意味着复杂性就越高。如果软件的单元没有事先进行测试,开发人员很可能会花费大量的时间仅仅是为了使软件能够运行,而任何实际的测试方案都无法执行。一旦软件可以运行了,开发人员又要面对这样的问题:在考虑软件全局复杂性的前提下对每个单元进行全面的测试。这是一件非常困难的事情,甚至在创造一种单元调用的测试条件的时候,要全面的考虑单元的被调用时的各种入口参数。

在软件集成阶段, 对单元功能全面测试的复杂程度远远的超过独立进行的单元测试过程。最后的结果是测试将无法达到它所应该有的全面性。一些缺陷将被遗漏,并且很多Bug将被忽略过去。

让我们类比一下,假设我们要清洗一台已经完全装配好的食物加工机器!无论你喷了多少水和清洁剂,一些食物的小碎片还是会粘在机器的死角位置,只有任其腐烂并等待以后再想办法。但我们换个角度想想,如果这台机器是拆开的,这些死角也许就不存在或者更容易接触到了,并且每一部分都可以毫不费力的进行清洗。

点评:如果每个开发人员都这样想,那么集成测试有可能进行不下去了,例如:系统集成后非常容易崩溃,无法进行测试。

另外,即使能够进行正常测试,需要说明的是bug发现得越晚,修改它所付出的代价越大。这样违背原则:尽早发现并修改bug,以减少软件开发成本。

5. 单元测试的成本效率不高

一个特定的开发组织或软件应用系统的测试水平取决于对那些未发现的Bug的潜在后果的重视程度。这种后果的严重程度可以从一个Bug引起的小小的不便到发生多次的死机的情况。这种后果可能常常会被软件的开发人员所忽视(但是用户可不会这样),这种情况会长期的损害这些向用户提交带有Bug的软件的开发组织的信誉,并且会导致对未来的市场产生负面的影响。相反地,一个可靠的软件系统的良好的声誉将有助于一个开发组织获取未来的市场。

很多研究成果表明, 无论什么时候做出修改都要进行完整的回归测试, 在生命周期中尽早地对软件产品进行测试将使效率和质量得到最好的保证。Bug发现的越晚,修改它所需的费用就越高,因此从经济角度来看,应该尽可能早的查找和修改Bug。在修改费用变的过高之前,单元测试是一个在早期抓住Bug的机会。

相比后阶段的测试,单元测试的创建更简单,维护更容易,并且可以更方便的进行重复。从全程的费用来考虑,相比起那些复杂且旷日持久的集成测试,或是不稳定的软件系统来说, 单元测试所需的费用是很低的。

点评:单元测试是一个在早期抓住bug的机会,符合原则:尽早发现并修改bug,以减少软件开发成本。

(二)单元测试不能做什么

1. 单元测试解决不了设计失误带来的问题,即使最低劣的设计也可能通过单元测试。

2. 单元测试解决不了由于设计和编码不当带来的性能问题。

3. 单元测试不能解决产品的可用性和易用性问题。

4. 单元测试通常不能解决类和类的一些逻辑问题。

5. 单元测试不能用来测试需要很长时间才能完成的操作(例如:数据连接超时)。

(三)好的单元测试(Visual Studio)标准

1. 单元测试应该在最低的功能/参数上验证程序的正确性

单元测试应该测试程序中最基本的单元,如C#中的类。(作为扩展来说,也可以适当测试一些类与类之间简单的逻辑)

2. 单元测试应该由最熟悉代码的人(程序的作者)来写

3. 单元测试过后,机器状态保持不变

这样就可以重复不断地运行单元测试,例如:如果单元测试创建了一些临时文件或目录,应该在Teardown阶段将它们删除。

4. 单元测试应该要快

5. 单元测试应该产生可重复的、一致的结果

6. 独立性,单元测试的运行/通过/失败不依赖于别的测试,可以人为构造数据,以保持单元测试的独立性

7. 单元测试应该覆盖所有代码路径,包括错误处理路径

8. 单元测试应该集成到自动测试的框架中

单元测试自动化后,这样每个人都能很容易地运行它,并且每天都可以自动运行。

9. 单元测试必须和产品代码一起保存和维护

(四)怎样进行C++单元测试

1. 创建Win32_dll项目,该项目模拟待测试的Win32动态连接库项目

a) 启动Visual Studio 2008,选择[文件] -> [新建] -> [项目],进入如下图界面:

b) 按上图中的命名输入,点击[确定],进入下面的界面:

c) 点击[完成],生成Win32的动态连接库(DLL)项目Win32_dll。

d) 打开Win32_dll.cpp,增加几个单元用于演示单元测试,增加之后的Win32_dll.cpp文件内容如下(红色粗体部分表示增加内容):

// Win32_dll.cpp : 定义DLL 应用程序的导出函数。

//

#include "stdafx.h"

__declspec(dllexport) int add(int a, int b)

{

return a + b;

}

__declspec(dllexport) int sub(int a, int b)

{

return a - b;

}

class __declspec(dllexport) calc

{

public:

int add_or_sub(int a, int b, BOOL is_add);

};

int calc::add_or_sub(int a, int b, BOOL is_add)

{

if(is_add)

{

return a + b;

}

return a - b;

}

e) 这样,待测试的Win32_dll项目就完成了。

f) 为了能够运行、统计Win32_dll项目的代码覆盖率,对其进入如下设置:选择Win32_dll项目 -> 右键菜单 -> 属性 -> 进入如下界面:

g) 按上图进行设置,点击[确定]即可。

h) 编译Win32_dll项目,在解决方案目录下的Debug目录(本例为e:\test\CppUnitTest\Debug\)中生成了Win32_dll.lib和Win32_dll.dll。

2. 创建MFC_dll项目,该项目模拟待测试的MFC动态连接库项目

a) 右键点击[解决方案] -> [添加] -> [新建项目],进入如下图界面:

b) 按上图进行设置,点击[确定],进入下面的界面:

c) 点击[完成],生成MFC的动态连接库(DLL)项目MFC_dll。

d) 右键点击[项目MFC_dll]下的[源文件] -> [添加] -> [新建项目]出现如下的界面:

e) 点击[添加] -> 项目中生成了MyPtrList.cpp,文件内容为空。

f) 打开MyPtrList.cpp,增加一个MyPtrList类用于演示单元测试,增加之后的MyPtrListl.cpp文件内容如下(红色粗体部分表示增加内容):

#include "stdafx.h"

class __declspec(dllexport) MyPtrList : public CPtrList

{

public:

MyPtrList(int nBlockSize);

};

MyPtrList::MyPtrList(int nBlockSize) : CPtrList(nBlockSize)

{

}

g) 这样,待测试的MFC_dll项目就完成了。

h) 为了能够运行、统计该项目的代码覆盖率,对该项目进入如下设置:选择MFC_dll项目 -> 右键属性 -> 进入如下界面:

i) 按上图进行设置,点击[确定]即可。

j) 编译MFC_dll项目,在解决方案目录下的Debug目录(本例为e:\test\CppUnitTest\Debug\)中生成了MFC_dll.lib和MFC_dll.dll。

3. 创建单元测试项目MyUnitTest,用于测试Win32_dll项目和MFC_dll项目中的单元代码

a) 选项Visual Studio菜单[测试] -> [新建测试] -> 进入如下图界面:

b) 点击[确定] -> 弹出新建测试项目 -> 输入MyUnitTest,点击[创建],生成了单元测试项目MyUnitTest。

c) 右键点击[项目MyUnitTest]下的[源文件] -> [添加] -> [已有项目],选择e:\test\CppUnitTest\Debug\下的Win32_dll.lib和MFC_dll.lib,选择不需要生成规则即可。

d) 打开stdafx.h,增加下面的内容(红色粗体部分表示增加内容,特别说明:增加内容以后,stdafx.h的内容和新生成的MFC应用程序中的stdafx.h内容完全相同):

// stdafx.h : 标准系统包含文件的包含文件,

// 或是经常使用但不常更改的

// 项目特定的包含文件

#pragma once

#ifndef VC_EXTRALEAN

#define VC_EXTRALEAN// 从Windows 标头中排除不常使用的资料

#endif

// 如果您必须使用下列所指定的平台之前的平台,则修改下面的定义。

// 有关不同平台的相应值的最新信息,请参考MSDN。

#ifndef WINVER// 允许使用Windows 95 和Windows NT 4 或更高版本的特定功能。

#define WINVER 0x0400//为Windows98 和Windows 2000 及更新版本改变为适当的值。

#endif

#ifndef _WIN32_WINNT// 允许使用Windows NT 4 或更高版本的特定功能。

#define _WIN32_WINNT 0x0400//为Windows98 和Windows 2000 及更新版本改变为适当的值。

#endif

#ifndef _WIN32_WINDOWS// 允许使用Windows 98 或更高版本的特定功能。

#define _WIN32_WINDOWS 0x0410 //为Windows Me 及更新版本改变为适当的值。

#endif

#ifndef _WIN32_IE// 允许使用IE 4.0 或更高版本的特定功能。

#define _WIN32_IE 0x0400//为IE 5.0 及更新版本改变为适当的值。

#endif

#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS// 某些CString 构造函数将是显式的

// 关闭MFC 对某些常见但经常被安全忽略的警告消息的隐藏

#define _AFX_ALL_WARNINGS

#include <afxwin.h>         // MFC 核心和标准组件

#include <afxext.h>         // MFC 扩展

#include <afxdtctl.h>// Internet Explorer 4 公共控件的MFC 支持

#ifndef _AFX_NO_AFXCMN_SUPPORT

#include <afxcmn.h>// Windows 公共控件的MFC 支持

#endif // _AFX_NO_AFXCMN_SUPPORT

e) 打开TestWin32_dll.cpp,增加几个单元测试,增加之后的TestWin32_dll.cpp文件内容如下(红色粗体部分表示增加内容):

#include "stdafx.h"

__declspec(dllimport) int add(int a, int b);

__declspec(dllimport) int sub(int a, int b);

class __declspec(dllimport) calc

{

public:

int add_or_sub(int a, int b, BOOL is_add);

};

class __declspec(dllimport) MyPtrList : public CPtrList

{

public:

MyPtrList(int nBlockSize);

};

using namespace System;

using namespace System::Text;

using namespace System::Collections::Generic;

using namespace Microsoft::VisualStudio::TestTools::UnitTesting;

namespace MyUnitTest

{

[TestClass]

public ref class TestWin32_dll

{

private:

TestContext^ testContextInstance;

public

/// <summary>

///获取或设置测试上下文,该上下文提供

///有关当前测试运行及其功能的信息。

///</summary>

property Microsoft::VisualStudio::TestTools::UnitTesting::TestContext^ TestContext

{

Microsoft::VisualStudio::TestTools::UnitTesting::TestContext^ get()

{

return testContextInstance;

}

System::Void set(Microsoft::VisualStudio::TestTools::UnitTesting::TestContext^ value)

{

testContextInstance = value;

}

};

#pragma region Additional test attributes

//

//编写测试时,还可使用以下附加属性:

//

//在运行类中的第一个测试之前,使用ClassInitialize 来运行代码

//[ClassInitialize()]

//static void MyClassInitialize(TestContext^ testContext) {};

//

//在类中的所有测试都已运行之后,使用ClassCleanup 来运行代码

//[ClassCleanup()]

//static void MyClassCleanup() {};

//

//在运行每个测试之前,使用TestInitialize 来运行代码

//[TestInitialize()]

//void MyTestInitialize() {};

//

//在每个测试运行完之后,使用TestCleanup 来运行代码

//[TestCleanup()]

//void MyTestCleanup() {};

//

#pragma endregion 

[TestMethod]

void TestAdd()

{

Assert::AreEqual(add(1,2), 3);

};

[TestMethod]

void TestSub()

{

Assert::AreEqual(sub(1,2), -1);

};

[TestMethod]

void TestAddOrSub()

{

calc c;

Assert::AreEqual(c.add_or_sub(1, 2, TRUE), 3);

Assert::AreEqual(c.add_or_sub(1, 2, FALSE), -1);

};

[TestMethod]

void TestMyPtrList()

{

int a = 123;

MyPtrList list(3);

Assert::AreEqual(list.GetCount(), 0);

list.AddTail(&a);

Assert::AreEqual(list.GetCount(), 1);

POSITION pos = list.GetHeadPosition();

Assert::IsTrue(pos != NULL);

Assert::AreEqual(*(int *)(list.GetAt(pos)), 123);

list.RemoveAll();

};

};

}

f) 这样,单元测试项目MyUnitTest项目就完成了。

g) 为了能够运行、统计该Win32_dll项目和MFC_dll项目的代码覆盖率,对MyUnitTest项目进入如下设置:选择MyUnitTest项目 -> 右键属性 -> 进入如下界面:

为了支持MFC和混和型单元测试项目,按下图进行设置:

h) 点击[确定]完成MyUnitTest项目配置。

i) 选择Visual Studio菜单[测试] -> [编辑测试运行配置] ->本地测试运行(LocalTestRun.testrunconfig)按下图进行选择,表示要对MFC_dll.dll和Win32_dll.dll进行代码覆盖统计:

j) 按顺序重新生成Win32_dll项目、MFC_dll项目、MyUnitTest项目,这样所有项目就生成成功了。

4. 调试和在本地运行单元测试

a) 设置MyUnitTest项目为启动项目。

b) 你可以在需要调试的地方设置断点,之后就可以F5进行调试,并观察本地运行结果。

5. 查看代码覆盖率

a) 选择Visual Studio菜单[测试] -> [运行] -> [解决方案中的所有测试] -> 结果如下图所示:

 

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值