关键词: VSTS 单元测试 缺陷 软件质量
在开始本文之前,我们有必要先探讨几个问题:
² 为什么需要做单元测试?
² 单元测试应该让谁来完成?
² 做单元测试的工具有哪些?
一.引言
笔者就自己对于单元测试的认识,简单做如下回答:
ü 我们熟知的“软件测试的10大原则”中第一条就是“测试是一个持续进行的过程,而不是一个阶段”,就是说软件测试需要尽早的参与研发投入,V&V模型就很好的为我们描述了这个观点。根据缺陷放大理论,在白盒测试阶段发现BUG是投入成本最低也是最高效的。
ü 51testing的论坛上有一个贴,讨论过这个问题,笔者发现大多数的公司根本不进行白盒测试,或者进行白盒测试的也是开发人员完成的。更为奇怪的是,我在几年前参加的程序员考试竟然也认为应该把单元测试的任务移交给开发人员。
笔者认为,测试人员之所以选择测试,就是因为我们比开发人员更加具有对于缺陷的敏感性,并且对缺陷的认识程度远高于开发人员;另外,开发人员对于自己开发的程序具有很显著的思维定势,单元测试的职责理所应当让测试人员来完成。
ü 单元测试的工具主流的是XUnit,还有C++Test、Insure++、CompuwareDevParterner、Purify、PC-Lint、LogiScope、TestBed等也是很强大的单元测试工具,另外基于Java也有自己的工具:Agitator。此外,笔者曾用高级语言调用TCL编译器,在TCL脚本语言环境创建单元测试用例也是个不错的选择。
二.VSTS单元测试简介
Team版的VS2005里面包含了完整的Test功能,具体有:Unit Test,WebTest和LoadTest。这一整套的测试基本涵盖了软件开发会使用到的测试功能。
今天要介绍的是VSTS自带的单元测试工具(Unit Test),它最大的好处就是与开发的代码无缝连接,并且能自动创建单元测试环境,能够让不懂编程的测试人员也轻松进行单元测试。
三.测试实战
下面我们就通过一个实例来具体体验VSTS单元测试。
在测试过程中,将按照如下顺序进行:
被测系统简介->被测试函数分析->测试用例的编写(黑盒/白盒测试方法)->进行单元测试
1. 被测系统简介
被测系统是“快速价格查询系统”,通过选定座位号的两个值,系统自动算出价格(界面如下图)。“座位号”下拉框的可选范围分别为“A”~“F”和“0”~“9”
如上图所示,“应付价格”=“价格基数”ד座位号权值”
2. 被测试函数分析
我们需要测试的就是权值计算的函数,开发人员在函数申明中是这样描述的:
//权值计算
/// <summary>
/// 如果X是A、F,则靠窗,纵向权值=1.8
/// 如果X是B、E,则靠中间,纵向权值=1.2
/// 如果X是C、D,则靠走廊,纵向权值=0.9
/// 如果Y是0~2,则靠头部,横向权值=1.6
/// 如果Y是7~9,则靠尾部,横向权值=0.7
/// 如果Y是3~6,则靠中间,横向权值=1.1
/// 权值=纵向权值*横向权值
/// </summary>
///<param name="x">纵列值</param>
///<param name="y">横列值</param>
staticdouble position(char x,int y)
{
……
}
3. 测试用例的编写(黑盒测试方法)
也许有人会产生疑问:单元测试怎么可以用黑盒测试方法设计测试用例呢?黑盒测试不是做系统测试的吗?
其实我觉得测试方法都是一家,我们在设计用例的时候要秉承一个原则,那就是:测试是无穷尽的,我们要以尽量少(20%)的测试用例发现尽量多(80%)的缺陷。在设计用例的时候大可以集成天下,只要是为高效发现缺陷的用例设计思路都可以采纳,不必拘泥于特定的条条框框。我们只要把函数具体的实现方式抽象成一个黑盒,即可用黑盒设计思路。
根据开发人员提供的描述,我脑海中即刻浮现出一幅图:
限于篇幅有限,且重点不在于用例设计上,笔者在这里象征性的用划分等价类的方法设计了5个测试用例:
注:实际测试过程中可以结合边界值、正交法、错误猜测、因果图等方法设计出更多测试用例进行测试
4. 测试用例的编写(白盒测试方法)
白盒测试用例设计方法少不了函数流程图:
限于篇幅有限,且重点不在于用例设计上,笔者在这里象征性的用语句覆盖的方法设计了3个测试用例:
注:实际测试过程中可以结条件覆盖、判定覆盖、判定/条件覆盖、条件组合覆盖、路径覆盖等方法设计出更多测试用例进行测试
5. 进行单元测试
5.1创建单元测试
至此,我们简单设计了8个测试用例,接下来就要在VSTS里创建单元测试了
1. 首先,鼠标留在被测函数staticdouble position(char x,int y)上,点击右键,弹出如下图所示菜单:
2. 选择“创建单元测试”,弹出如下对话框:
在这里我们可以看到,无论开发的代码是用何种语言写的,只要是用VS2005创建的,我们测试的时候,可以选择C#、VB、C++都可以实现,这正是VS2005强大之处之一。
3. 勾选上我们需要测试的函数,点击“确定”,弹出如下对话框:
4. 输入测试项目名后,点击“创建”,在“解决方案资源管理器”中,我们可以看到创建成功了一个以我们刚才键入的名字命名的测试方案(如下图红框所示)
5.2代码分析
在自动生成的form1Test.cs里,有这么一段代码:
/// <summary>
///position (char, int)的测试
///</summary>
[DeploymentItem("WindowsApplication1.exe")]
[TestMethod()]
public void positionTest()
{
char x = '/0'; // TODO: 初始化为适当的值
int y = 0; // TODO: 初始化为适当的值
double expected = 0;
double actual;
actual = TestProject3.WindowsApplication1_Form1Accessor.position(x, y);
Assert.AreEqual(expected, actual, "WindowsApplication1.Form1.position 未返回所需的值。");
Assert.Inconclusive("验证此测试方法的正确性。");
}
这就是单元测试的测试代码的主体部分,我们来详细分析一下:
[TestMethod()]:说明了以下代码是一个测试用例,在form1Test.cs中的“#region附加测试属性”里还有很多带有中括号的方法我们在后文再讲述
char x = '/0'; // TODO: 初始化为适当的值
int y = 0; // TODO: 初始化为适当的值:这两句是被测函数的输入参数,需要我们去修改它的值,说白了,也就是我们输入测试用例的地方
在这里我们可以看到:VSTS自动为x赋了初值’/0’,为y赋了初值0,我们要做的,只是更改它的值就可以了,VSTS自动为char类型加上了单引号,显示出非常人性化和智能化
double expected = 0;
double actual;:这两句话浅显易懂,前一句话是定义了期望值和对它进行初始化,后一句话是定义了实际值。对expected进行了赋值=0实际上是在提醒我们:这里是需要测试人员在进行测试时需要更改的地方——多么的人性化!
可能有人要问了:那么实际值actual改怎么去赋值?下面的这句语句就是在对它进行赋值:
actual = TestProject3.WindowsApplication1_Form1Accessor.position(x, y);
太棒了,现在期望值和实际值都已经获取到了,该对他们进行比对了吧?没错,下面的这句语句就实现了这个功能:
Assert.AreEqual(expected, actual,"WindowsApplication1.Form1.position 未返回所需的值。");
Assert在这里可以理解成断言:在VSTS里做单元测试是基于断言的测试。在MSDN网站上,有这样的描述:“可定义为“实或您相信为事实的内容”。从逻辑角度看,请考虑该语句“when I do {x}, I expect {y} as a result”。”查阅帮助文档,可以看到有以下这些断言:
乍看起来有点摸不着头脑,举个简单的例子,你就会明白:比如说,我们在刚才的测试用例1中,输入的是(A,0),预期输出是2.88,那么现在expected=2.88,实际值是actual,我们认为expected=actual测试才算通过,如果不相等的话,把“测试用例不通过”这句话纪录进测试结果,那么我们就把这句语句改为:Assert.AreEqual(expected, actual, "测试用例不通过");
简单的说,断言就是我们去定义为测试通过的准则
帮助文档里有各个断言的描述:
现在再来看最后一句话:
Assert.Inconclusive("验证此测试方法的正确性。");
对照上表就很容易看懂了,是指“表示无法证明为 true或 false 的测试结果。”
如果在创建单元测试之初点击“设置”,把“默认情况下把所有测试结果标记为没有结论”这个钩去掉(见下图)
那么创建出来的单元测试就没有Assert.Inconclusive("验证此测试方法的正确性。");这句话。没关系,我们手动把它删除
5.3进行测试初体验
根据刚才的语句分析,我们现在就更改测试代码,输入第一个测试用例:
[DeploymentItem("WindowsApplication1.exe")]
[TestMethod()]
public void positionTest()
{
char x = 'B'; // TODO: 初始化为适当的值
int y = 7; // TODO: 初始化为适当的值
double expected = 0.84;
double actual;
actual = TestProject3.WindowsApplication1_Form1Accessor.position(x, y);
Assert.AreEqual(expected, actual,"实际值:"+actual+" "+"期望值:"+expected);
}
用例输完了,我们先试一下,看看结果:点击菜单栏“测试”->“启动选定的测试项目(不调试)”(如下图)
然后我们在最下面的“测试结果栏”可以看到如下图所示的测试结果:
双击它可以看到更详细的测试结果(如下图):
测试成功通过了,耶!真是激动人心!
不相信自己?好的,那我们改一下代码,把double expected = 0.84;改成double expected = 8.88;,然后再执行一遍测试,看看是什么结果:
双击它:
哈哈,VSTS明察秋毫,这回相信自己了吧!
5.3输入测试用例和进行测试
现在我们要把刚才的8个测试用例全部输入
测试用例:
怎么输呢?很简单,只要把刚才那段测试代码复制、粘贴一下,更改输入值和预期值就可以了。需要注意的是测试函数名不能相同。我这里写两个测试用例来举个例子:
测试代码:
[TestMethod()]
public void positionTest1()
{
char x = 'A'; // TODO: 初始化为适当的值
int y = 0; // TODO: 初始化为适当的值
double expected = 2.88;
double actual;
actual = TestProject3.WindowsApplication1_Form1Accessor.position(x, y);
Assert.AreEqual(expected, actual, "实际值:"+actual+" "+"期望值:"+expected);
}
[TestMethod()]
public void positionTest2()
{
char x = 'B'; // TODO: 初始化为适当的值
int y = 3; // TODO: 初始化为适当的值
double expected = 1.32;
double actual;
actual = TestProject3.WindowsApplication1_Form1Accessor.position(x, y);
Assert.AreEqual(expected, actual, "实际值:" + actual +" " + "期望值:" + expected);
}
就是这样,把所有的测试用例全部输入后,进行测试,根据测试结果,填写《测试报告》。
大功告成,让我们举杯庆祝一下吧!
四.“附加测试属性”的实践
在Form1Test.cs中有这么一个功能,这又是VSTS单元测试之强大的地方。
我们可以点开它,点开后我们看到如下的代码:
#region附加测试属性
//编写测试时,可使用以下附加属性:
//使用 ClassInitialize在运行类中的第一个测试前先运行代码
//[ClassInitialize()]
//public static void MyClassInitialize(TestContext testContext)
//{
//}
//使用ClassCleanup 在运行完类中的所有测试后再运行代码
//[ClassCleanup()]
//public static void MyClassCleanup()
//{
//}
//使用 TestInitialize在运行每个测试前先运行代码
//[TestInitialize()]
//public void MyTestInitialize()
//{
//}
//使用 TestCleanup在运行完每个测试后运行代码
//[TestCleanup()]
//public void MyTestCleanup()
//{
//}
#endregion
这是干什么的呢?呵呵,其实它已经说的很明白了,这是“附加测试属性”。默认都是被注释掉的,只要我们取消注释就可以使用了。具体的属性有:[ClassInitialize()]、[ClassCleanup()]、[TestInitialize()]、[TestCleanup()]。他们的作用在它们上面的注释里都已经讲的很明白了,笔者在这里就不展开叙述了。
微软之所以加进来这么个功能,以我之所见,是为了加大测试的灵活性。
我举一例,大家就会看得很明白:现在我们打算把测试报告输出到外部文档去,这样方便日后的缺陷跟踪。可以修改代码如下:
#region附加测试属性
//编写测试时,可使用以下附加属性:
//使用 ClassInitialize在运行类中的第一个测试前先运行代码
[ClassInitialize()]
public static void MyClassInitialize(TestContext testContext)
{
StreamWriter m_streamWriter = new StreamWriter(@"C:/log.txt",true);
m_streamWriter.Write("Test Time,");
m_streamWriter.WriteLine(DateTime.Now);
m_streamWriter.Flush();
m_streamWriter.Close();
}
//使用 ClassCleanup在运行完类中的所有测试后再运行代码
[ClassCleanup()]
public static void MyClassCleanup()
{
StreamWriter m_streamWriter =new StreamWriter(@"C:/log.txt",true);
m_streamWriter.WriteLine("");
m_streamWriter.Write("Total Passed:,");
m_streamWriter.WriteLine(Convert.ToString(Passed) +",");
m_streamWriter.Write("Total NotPassed:,");
m_streamWriter.WriteLine(Convert.ToString(NotPassed) +",");
m_streamWriter.Flush();
m_streamWriter.Close();
}
//使用 TestInitialize在运行每个测试前先运行代码
[TestInitialize()]
public void MyTestInitialize()
{
StreamWriter m_streamWriter = new StreamWriter(@"C:/log.txt",true);
if (actual == expected)
{
m_streamWriter.WriteLine("Test Passed");
Passed++;
}
else
{
m_streamWriter.Write("Test NotPassed,");
m_streamWriter.Write("Input X:,");
m_streamWriter.Write(Convert.ToString(x) +",");
m_streamWriter.Write("Input Y:,");
m_streamWriter.Write(Convert.ToString(y) +",");
m_streamWriter.Write("Expected:,");
m_streamWriter.Write(Convert.ToString(expected) +",");
m_streamWriter.Write("Actual:,");
m_streamWriter.Write(Convert.ToString(actual));
m_streamWriter.WriteLine("");
NotPassed++;
}
m_streamWriter.Flush();
m_streamWriter.Close();
}
//使用 TestCleanup在运行完每个测试后运行代码
//[TestCleanup()]
//public void MyTestCleanup()
//{
//}
#endregion
同时引用:using System.IO;
申明变量:staticint Passed=0;
static int NotPassed=0;
static double expected;
static double actual;
static char x;
static int y;
并且把测试用例里的对于x、y、actual、expected的申明删除
这时候我们再点击进行测试后,就会在c:/下面生成log.txt,内容如下:
Test Time,2007-7-7 23:29:57
Test Passed
Test NotPassed,Input X:,B,Input Y:,3,Expected:,1.31,Actual:,1.32
Test Passed
Test NotPassed,Input X:,A,Input Y:,0,Expected:,8.88,Actual:,2.88
Test Passed
Total Passed:,3,
Total NotPassed:,2,
可能大家在以上文本或者代码中感到奇怪:为什么要加进去那么多的逗号?
原因在此:把log.txt更名为log.csv,那么,就可以用excel来打开它(如下表): (这里放不进来)
这样清晰多了吧?而且更加易于维护
笔者在这里只是拿外部文件的输出作为一个例子,“附加测试属性”的其它有意义的用途欢迎大家共同探讨
结束语
VSTS的单元测试的功能之强大由此可见一斑,本人更希望以此文能召唤起更多的国内公司能更加重视软件测试,更加规范软件测试,把我国的软件测试行业做强、做精!
参考文献:
郑人杰 .计算机软件测试技术 .清化大学出版社,1990
古乐 .软件测试技术概论 .清化大学出版社,2004
http://www.microsoft.com/china/MSDN/library
鄙人微博:http://weibo.com/quicktest,愿与大家一起探讨