实验五 单元测试
一、实验目的
1)掌握单元测试的方法
2) 学习XUnit测试原理及框架;
3)掌握使用测试框架进行单元测试的方法和过程。
二、实验内容与要求
1、了解单元测试的原理与框架
1.1 单元测试原理
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。
单元测试的内容包括
模块接口测试、局部数据结构测试、路径测试、错误处理测试、边界测试
(1)模块接口测试
模块接口测试是单元测试的基础。只有在数据能正确流入、流出模块的前提下,其他测试才有意义。模块接口测试也是集成测试的重点,这里进行的测试主要是为后面打好基础。测试接口正确与否应该考虑下列因素:
-输入的实际参数与形式参数的个数是否相同
-输入的实际参数与形式参数的属性是否匹配
-输入的实际参数与形式参数的量纲是否一致
-调用其他模块时所给实际参数的个数是否与被调模块的形参个数相同;
-调用其他模块时所给实际参数的属性是否与被调模块的形参属性匹配;
-调用其他模块时所给实际参数的量纲是否与被调模块的形参量纲一致;
-调用预定义函数时所用参数的个数、属性和次序是否正确;
-是否存在与当前入口点无关的参数引用;
-是否修改了只读型参数;
-对全程变量的定义各模块是否一致;
-是否把某些约束作为参数传递。
如果模块功能包括外部输入输出,还应该考虑下列因素:
-文件属性是否正确;
-OPEN/CLOSE语句是否正确;
-格式说明与输入输出语句是否匹配;
-缓冲区大小与记录长度是否匹配;
-文件使用前是否已经打开;
-是否处理了文件尾;
-是否处理了输入/输出错误;
-输出信息中是否有文字性错误。
-局部数据结构测试;
-边界条件测试;
-模块中所有独立执行通路测试;
(2)局部数据结构测试
检查局部数据结构是为了保证临时存储在模块内的数据在程序执行过程中完整、正确,局部功能是整个功能运行的基础。重点是一些函数是否正确执行,内部是否运行正确。局部数据结构往往是错误的根源,应仔细设计测试用例,力求发现下面几类错误:
-不合适或不相容的类型说明;
-变量无初值;
-变量初始化或省缺值有错;
-不正确的变量名(拼错或不正确地截断);
-出现上溢、下溢和地址异常。
(3)边界条件测试
边界条件测试是单元测试中最重要的一项任务。众所周知,软件经常在边界上失效,采用边界值分析技术,针对边界值及其左、右设计测试用例,很有可能发现新的错误。边界条件测试是一项基础测试,也是后面系统测试中的功能测试的重点,边界测试执行的较好,可以大大提高程序健壮性。
(4)独立路径测试
在模块中应对每一条独立执行路径进行测试,单元测试的基本任务是保证模块中每条语句至少执行一次。测试目的主要是为了发现因错误计算、不正确的比较和不适当的控制流造成的错误。具体做法就是程序员逐条调试语句。常见的错误包括:
-误解或用错了算符优先级;
-混合类型运算;
-变量初值错;
-精度不够;
-表达式符号错。
(5)错误处理测试
检查模块的错误处理功能是否包含有错误或缺陷。例如,是否拒绝不合理的输入;出错的描述是否难以理解、是否对错误定位有误、是否出错原因报告有误、是否对错误条件的处理不正确;在对错误处理之前错误条件是否已经引起系统的干预等。
通常单元测试在编码阶段进行。在源程序代码编制完成,经过评审和验证,确认没有语法错误之后,就开始进行单元测试的测试用例设计。利用设计文档,设计可以验证程序功能、找出程序错误的多个测试用例。对于每一组输入,应有预期的正确结果。
1.2 测试框架
xUnit是各种代码驱动测试框架的统称,这些框架可以测试 软件的不同内容(单元),比如函数和类。xUnit框架的主要优点是,它提供了一个自动化测试的解决方案。可以避免多次编写重复的测试代码。
TestCase(具体的测试用例)去使用framwork
TestCase执行后会有TestResult
使用TestSuite控制TestCase的组合
TestRunner执行器,负责执行case
TestListener过程监听,监听case成功失败以及数据结果,输出到结果报告中
Unit测试框架包括四个要素:
(1)测试目标(对象)
一组认定被测对象或被测程序单元测试成功的预定条件或预期结果的设定。Fixture就是被测试的目标,可以是一个函数、一组对象或一个对象。 测试人员在测试前应了解被测试的对象的功能或行为。
(2)测试集
测试集是一组测试用例,这些测试用例要求有相同的测试Fixture,以保证这些测试不会出现管理上的混乱。
(3)测试执行
单个单元测试的执行可以按下面的方式进行:
第一步 编写 setUp() 函数,目的是:建立针对被测试单元的独立测试环境;举个例子,这可能包含创建临时或代理的数据库、目录,再或者启动一个服务器进程。
第二步 编写所有测试用例的测试体或者测试程序;
第三步 编写tearDown()函数,目的是:无论测试成功还是失败,都将环境进行清理,以免影响后续的测试;
(4)断言
断言实际上就是验证被测程序在测试中的行为或状态的一个函数或者宏。断言的失败会引发异常,终止测试的执行。
1.3 面向特定语言的,基于xUnit框架的自动化测试框架
Junit : 主要测试用Java语言编写的代码
CPPunit:主要测试用C++语言编写的代码shi
unittest , PyUnit:主要测试用python语言编写的代码
MiniUnit: 主要用于测试C语言编写的代码
三、实验过程
本次实验是测试之前实验二的生四则运算代码,由于我们组在编写四则运算时选用的是c++作为编程语言,故而本次测试我们选用的框架是CPPUnit框架。
1.选择的测试框架介绍及安装过程
(1)介绍:
极限编程(XP)推崇测试优先原则,由此引发了软件开发方法从传统的瀑布模型转向以测试为驱动的敏捷开发模式的革命。在这场软件开发方法革命中,以xUnit系列的单元测试框架是一切的中心。xUnit的成员有很多,如JUnit,NUnit、PythonUnit,HtmlUnit,HttpUnit等。CppUnit就是xUnit家族中的一员,它是一个专门面向C++的单元测试框架。CppUnit是Micheal Feathers由JUnit移植过来的一个在GNU LGPL条约下的并在sourcefogre网站上开源的C++单元测试框架。
(2)安装过程
我选择了使用vs2010来安装cppunit。
①下载cppunit
我是在官网下载的cppunit最新版本(cppunit-1.12.1.tar.gz)网址如下:
https://sourceforge.net/projects/cppunit/files/cppunit/这个网站我用无线网打不开,但是电脑连接了手机热点就能打开了。如图选择1.12.1版本下载
下载后将其解压。
②打开.dsw文件
进入解压后的文件夹下,然后进入src文件夹里,将CppUnitLibraries.dsw文件用vs2010打开。vs2010打开该文件时会询问是否转换,选择是。
③修改目标文件名
右键cppunit,选择属性,在弹出窗口将目标文件名由$(ProjectName)改成$(ProjectName)d,如下图所示
同样的将cppunit_dll的目标文件名改成cppunitd_dll
之后将DllPlugInTester改成$(ProjectName)d_dll
将TestPlugInRunner的改成$(ProjectName)d
将TestRunner的改成$(ProjectName)d
④修改DSPlugin
右击DSPlugin->属性->配置属性->链接器->高级->无入口点,由“否”改成“是”。如图。
⑤修改TestRunner
在vs中打开TestRunner\UserInterface\ MsDevCallerListCtrl.cpp,在67行中将version由7.0改成8.0,如下
⑥编译lib库
在vs中选择生成-->批生成,全选后点击生成,如下
编译的过程可能需要一定的时间,这过程会有提示说错误或者失败,不过无伤大雅。我的lib库编译后结果如下
进入cppunit-1.12.1\lib文件夹下,可以看到我编译后的文件有如下这些。我们在使用cppunit时主要使用cppunitd.lib,故而这个文件一定要有。
至此,windows版cppunit安装编译成功。
⑦测试
cppunit有自带的测试example,可以测试cppunit是否能使用。
打开cppunit-1.12.1\examples\money文件夹的money.dsw文件
修改项目目标文件名
右键money工程名,配置属性-->常规-->目标文件名,将$(ProjectName)改成$(ProjectName)d
运行money工程。F5或ctrl+f5调试,出现下图即测试成功。
2.测试用例
测试代码:
源码:
#include "iostream" #include<stdlib.h> #include<time.h> #include<math.h> using namespace std; int main() { system("color 94"); cout<<endl; cout<<"**************************小学数学四则运算试题****************************"<<endl; cout<<endl; cout<<endl; Again: double Ques1=1; int Ques01; int Ques2=1; int Ques3=100; char Ques4; int Ques5=0; char Ques6='n'; char Ques7='n'; double ans=0; int right=0; cout<<" 一.请输入题目数量:"; cin>>Ques1; Ques01=floor(Ques1); // cout<<"请输入每行打印题目数(1-5):"; // cin>>Ques2; cout<<" 二.请输入算式中数值的最大值:"; cin>>Ques3; cout<<"`````````````````````````````````````````````````````````````````````````"<<endl; cout<<"开始答题:"<<endl; srand(time(NULL)); while(1) { if(Ques1<1) { cout<<"输入有误,请重新输入题目数量:"; cin>>Ques1; Ques01=floor(Ques1); } else { for(int j=0; j<Ques01; j++) { if(j!=0&&j%Ques2==0) { for(int i=0; i<Ques5; i++) { cout<<endl; } } int num1=rand()%Ques3; int num2=rand()%Ques3; int sign=rand()%4; switch(sign) { case 0: cout<<j+1<<":"<<" "<<num1<<"+"<<num2<<"="<<" "; cin>>ans; if(ans==num1+num2) { cout<<"正确"<<endl; right++; } else { cout<<"答错了!"<<endl; cout<<"正确答案是:"<<num1+num2<<endl; } break; case 1: if(Ques7=='y') { cout<<j+1<<":"<<" "<<num1<<"-"<<num2<<"="<<" "; cin>>ans; if(ans==num1-num2) { cout<<"正确"<<endl; right++; } else { cout<<"答错了!"<<endl; cout<<"正确答案是:"<<num1-num2<<endl; } } else { if(num1>num2) { cout<<j+1<<":"<<" "<<num1<<"-"<<num2<<"="<<" "; cin>>ans; if(ans==num1-num2) { cout<<"正确"<<endl; right++; } else { cout<<"答错了!"<<endl; cout<<"正确答案是:"<<num1-num2<<endl; } } else { cout<<j+1<<":"<<" "<<num2<<"-"<<num1<<"="<<" "; cin>>ans; if(ans==num2-num1) { cout<<"正确"<<endl; right++; } else { cout<<"答错了!"<<endl; cout<<"正确答案是:"<<num2-num1<<endl; } } } break; case 2: cout<<j+1<<":"<<" "<<num1<<"*"<<num2<<"="<<" "; cin>>ans; if(ans==num1*num2) { cout<<"正确"<<endl; right++; } else { cout<<"答错了!"<<endl; cout<<"正确答案是:"<<num1*num2<<endl; } break; case 3: if(num2!=0) { int t=rand()%30; int num3=num1*t; cout<<j+1<<":"<<" "<<num3<<"/"<<num1<<"="<<" "; cin>>ans; if(ans==(float)num3/(float)num1) { cout<<"正确"<<endl; right++; } else { cout<<"答错了!"<<endl; cout<<"正确答案是:"<<(float)num3/(float)num1<<endl; } } else { j--; } break; } } break; } } cout<<"共答对"<<right<<"道题;"<<"答错"<<Ques01-right<<"道题。"; cout<<"继续努力!"<<endl; cout<<endl; while(1) { cout<<"还需要继续出题还是退出?(y:继续出题;n:退出)"; cin>>Ques6; if(Ques6=='y') { goto Again; } if(Ques6=='n') { goto Exit; } else { cout<<"输入有误,请重新输入:"; } } Exit: return 0; }
#include <cppunit/TestCase.h> #include <cppunit/TestResult.h> #include <cppunit/TestResultCollector.h> #include <cppunit/TextOutputter.h> #include <cppunit/TestCaller.h> #include <cppunit/TestRunner.h> // 定义测试类 class StringTest : public CppUnit::TestFixture { public: void setUp() // 初始化 { m_str1 = "Hello, world"; m_str2 = "Hi, cppunit"; } void tearDown() // 清理 { } void testSwap() // 测试方法1 { std::string str1 = m_str1; std::string str2 = m_str2; m_str1.swap(m_str2); CPPUNIT_ASSERT(m_str1 == str2); CPPUNIT_ASSERT(m_str2 == str1); } void testFind() // 测试方法2 { int pos1 = m_str1.find(','); int pos2 = m_str2.rfind(','); CPPUNIT_ASSERT_EQUAL(5, pos1); CPPUNIT_ASSERT_EQUAL(2, pos2); } protected: std::string m_str1; std::string m_str2; }; int main(int argc, char* argv[]) { CppUnit::TestResult r; CppUnit::TestResultCollector rc; r.addListener(&rc); // 准备好结果收集器 CppUnit::TestRunner runner; // 定义执行实体 runner.addTest(new CppUnit::TestCaller<StringTest>("testSwap", &StringTest::testSwap)); // 构建测试用例1 runner.addTest(new CppUnit::TestCaller<StringTest>("testFind", &StringTest::testFind)); // 构建测试用例2 runner.run(r); // 运行测试 CppUnit::TextOutputter o(&rc, std::cout); o.write(); // 将结果输出 return rc.wasSuccessful() ? 0 : -1; }
test.h #include <cppunit/extensions/HelperMacros.h> class Test : public CPPUNIT_NS::TestFixture { CPPUNIT_TEST_SUITE(Test); CPPUNIT_TEST(testSolution1); CPPUNIT_TEST(testSolution2); CPPUNIT_TEST(testSolution3); CPPUNIT_TEST(testSolution4); CPPUNIT_TEST(testSolution5); CPPUNIT_TEST_SUITE_END(); public: void testSolution1(); void testSolution2(); void testSolution3(); void testSolution4(); void testSolution5(); };
#include "stdafx.h" using namespace System; using namespace System::Text; using namespace System::Collections::Generic; using namespace Microsoft::VisualStudio::TestTools::UnitTesting; namespace TestProject1 { [TestClass] public ref class UnitTest1 { 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 TestMethod1() { // // TODO: 在此处添加测试逻辑 // }; }; }
5、测试结果与分析
提交测试:
思考题:
比较以下二个工匠的做法,你认为哪种好?结合编码和单元测试,谈谈你的认识。
答:
我觉得工匠一的做法好,只有先定一个标准后面才更容易找错维护和衔接,只要拉一根线在那,谁看到那根线,都知道与它对齐就行了,而工匠二
虽然表面效率可能比工匠一快,但实际上,可能会切出很多问题的砖,又得重新返工浪费时间,而且没有给别人留下一个标准和方法,新手不容易上道