一 单元测试简介
单元测试是代码正确性验证的最重要的工具,也是系统测试当中最重要的环节。也是唯一需要编写代码才能进行测试的一种测试方法。在标准的开发过程中,单元测试的代码与实际程序的代码具有同等的重要性。每一个单元测试,都是用来定向测试其所对应的一个单元的数据是否正确。 单元测试是由程序员自己来完成,最终受益的也是程序员自己。可以这么说,程序员有责任编写功能代码,同时也就有责任为自己的代码编写单元测试。执行单元测试,就是为了证明这段代码的行为和我们期望的一致。单元测试还具有一下几个好处:
1、 能够协助程序员尽快找到BUG的具体位置
2、
能够让程序员对自己的程序更有自信
3、能够让程序员在提交项目之前就将代码变的更加健壮
4、 能够协助程序员更好的进行开发
5、 能够向其他程序员展现你的程序该如何调用
6、 能够让项目主管更了解系统的当前状况
? 1.1 能够协助程序员尽快找到BUG的具体位置
在没有单元测试的时代,我们大多数的错误都是通过操作页面的时候发现的。当我们发现一个错误的时候,会根据异常抛出的地点来确定是哪段代码出现了问题。但是大多数时候,我们不会所有方法中都使用Try块去处理异常(这一也是低效的)。因此一旦发现一个异常通常都是最顶层代码抛出的,但是错误往往又是在底层很深层次的某个对象中出现的。当我们找到了这个最初抛出异常的方法的时候,我们可能无法得知这段代码到底是哪里出了问题。只能逐行代码的去查找,一旦这个方法中使用的某个对象在外部有注册事件或者有其他的操作正在与当前方法同步进行,那么就更难发现错误真正的原因了。 有经验的程序员也会知道,大多数的时候,我们并不是真正的在编写新的代码,而是在修改旧的代码出现的错误。通常这个比例会大于2比8,这也是编写代码的时候的二八现象——编写代码的时间是二,而为这段代码找错误、修改错误所花费的时间却是八。 在这种状态之下,我们在找错误的时候会直接编译整个程序,然后通过界面逐步的操作到错误的地方然后再去查找代码中是否有错误。这样的找错误的方法效率非常低。但是当我们拥有单元测试的时候,我们就不需要通过界面去一步一步的操作,而是直接运行这个方法的单元测试,将输入的条件模拟成出现错误的时候输入的信息和调用的方法的形式,这样就可能很快的还原出错误。这样解决起来速度就提高了很多,每次找到错误都去修改单元测试,那么下次就不会再出现相同的错误了。如果通过模拟,单元测试也没有出现任何异常,这时也可以断定,并非该代码出现的错误,而是其他相关的代码出现的错误。我们只需再调试其他几个相关的代码的单元测试即可找到真正的错误。
? 1.2 能够让程序员对自己的程序更有自信
很多时候,当主管问我们程序会不会再出问题的时候,我们会很难回答。因为我们没法估计到系统还可能出现什么问题。但是如果这时我们为所有代码都编写了单元测试,而且测试代码的编写是按照标准去写的,这些测试又都能够成功的通过测试。那么我们就完全有自信说出我们的把握有多大。因为在测试代码中,我们已经把所有可能的情况都预料到了,程序代码中也将这些可能预料到的问题都解决了。因此我们会对自己的程序变得越来越自信。
? 1.3 能够让程序员在提交项目之前就将代买变得更加健壮
大多数程序员在编写代码的时候,都会先考虑最理想化情况下的程序该如何写,写完之后在理想状态下编译成功,然后输入理想的数据发现没有问题。他们就会自我安慰的说“完成了”。然后可能为了赶进度,就又开始作另外的程序了。时间久了这种理想化的程序就越来越多。一旦提交测试,就发现这里有错误那里有错误,然后程序员们再拿出时间来这里补个漏洞那里补个漏洞。而且在补漏洞的过程中,也可能继续沿用这种理想化的思路,就导致了补了这里又导致那里出问题的情况。但是如果在初期,我们就为每段代码编写单元测试,而且根据一些既定的标准去写,那么单元测试就会提前告诉程序员哪些地方会出现错误。那么他们可能在编写过程中就提前处理了那些非理想状态下的问题。这样我们的代码就会健壮很多。
?
1.4 能够协助程序员更好的进行开发
“码未动,测试现行。”这是极限编程中倡导的一种编程模式,为什么要这样呢?因为我们在编写单元测试的过程中,其实就是在设计我们的代码将要处理哪些问题。单元测试写的好,就代表你的代码写的好。而且你会根据单元测试的一些预先设想的情况去编写代码,就不会盲目的添加一个属性、添加一个方法了。
? 1.5 能够向其他程序员展现你的程序该如何调用
?通常情况下,单元测试代码中写的都是在各种情况下如何调用那段待测试的代码。因此这个单元测试同时也向其他人员展示了我们的代码该如何调用?在什么情况下会抛出什么异常?等等。这样一个单元测试就变成了一个代码性的帮助文档了。
? 1.6 能够让项目主管更了解系统当前的状况
传统的管理中,项目的进度、代码的质量都只是通过口头的形式传递到主管那里的。因此有时候主管获得的反馈可能事实。但是如果通过一个完善的单元测试系统,那么主管就可以通过查看单元测试的运行结果和单元测试的代码覆盖率来确定开发人员的工作是否真正完成。
2.1 创建单元测试
该工具可以对任何类、接口、结构等实体中的字段、属性、构造函数、方法等进行单元测试。创建单元测试大致可以分为两类: 一、 整体测试,整体测试是在类名称上右击鼠标,在下拉菜单中点击创建单元测试选项。这样就可以为整个类创建单元测试了,这时他会为整个类可以被测试的内容全部添加测试方法。开发人员直接在这些自动生成的测试方法中添加单元测试代码就可以了。 二、?单独测试,如果只想单独对某个方法、属性、字段进行测试,则可以将鼠标焦点放在这个待测试的项目名称之上,然后点击鼠标右键,在右键菜单中选择创建单元测试选项。这样就可以单独为某个方法创建单元测试了。
?
2.2 编写单元测试代码
创建完单元测试之后,就可以为单元测试编写测试代码了。具体的测试代码的编写标准会在第三章中介绍。
2.3 运行单元测试
单元测试代码编写完毕,就可以通过运行单元测试来进行测试了。需要运行单元测试的时候,需要打开测试管理器窗口。该窗口可以通过菜单中的“测试”-“窗口”——“测试管理器”来打开。打开该窗口之后,就可以在该窗口中看到我们所建立的单元测试的列表。我们可以在列表中勾选某个单元测试前面的复选框。然后右击鼠标在右键菜单中点击“调试选中的测试”或者“运行选中的测试”。
调试选中的测试的时候,我们可以在测试代码中或者我们自己的代码中添加断点并逐步运行以看其状态。
运行选中的测试只会运行测试并不能够进行测试,这时代码的运行是模拟真实软件运行的时候的情况执行的。我们可以根据我们的实际情况来选中执行哪种测试。
2.4 测试结果
运行了测试之后,我们需要查看这次测试的结果。我们可以通过点击菜单中的“测试”——“窗口”——“测试结果”来打开一个测试结果窗口。每次测试都会在测试结果中向我们显示一些记录。我们也可以通过双击这个测试结果,来查看详细的结果信息。
? 2.5 代码覆盖率
单元测试写的是否合理或者是否达到了要求的一个唯一的标准就是整个测试的代码覆盖率。代码覆盖率其实就是测试代码所运行到的实际程序路径的覆盖率。在实际程序中可能会有很多的循环、判断等分支路径。一个好的单元测试应该能够将所有可能的路径都将走到,这样就可以保证大多数情况都测试过了。 VS2005中也提供了查看代码覆盖率的工具。我们可以通过点击菜单中的“测试”——“窗口”——“代码覆盖率结果”来打开代码覆盖率查看的窗口。若要进行代码覆盖率的检查,我们必须进行设置,因为系统默认情况下是不进行代码覆盖率检测的。若要打开某个测试的代码覆盖率测试,我们必须点击菜单中的“测试”——“编辑测试运行配置”——“本地测试运行。。。。。。”来打开一个测试配置窗口。在该窗口左侧的列表中选中“代码覆盖率”就会显示代码覆盖率的设置。在这个配置中会显示当前解决方案中可以用来检测代码覆盖率的程序集,我们将需要进行覆盖率检测的程序集选中然后点击“应用”按钮就可以了。 设置完毕之后,我们就可以直接运行单元测试,测试通过后。我们就可以打开代码覆盖率结果窗口,在这里我们就能够看到这些测试覆盖了多少代码。当我们在这里双击某个类的时候,就可以看到VS已经将代码背景改变了颜色。显示为深棕色的代码就是没有覆盖到的代码,我们可以通过在单元测试代码中添加代码来想办法覆盖这些代码。这样一个单元测试的全过程就完成了。
虽然要进行单元测试的代码会是各种各样的,但是编写单元测试代码还是有规律可循的。测试的对象一般情况下分为方法(包含构造函数)、属性,因此我们按照这两个方向来确定单元测试的标准。 由于现在大多数开发人员还没有真正使用过.NET的单元测试工具,对于单元测试的了解程度也不高。因此我们这里也不便于制定非常多的标准。我们当前主要针对两大方面进行规范: 1、 哪些代码需要添加单元测试? 2、 单元测试代码的写法?
3.1 ?哪些代码需要添加单元测试
如果项目正处在一个最后冲刺阶段,主要的编码工作已经基本完成。因此要全面的添加单元测试,其实是比较大的投入。所以单元测试不能一次性的全部加上,我们只能通过一步一步的来进行测试。 第一步,应该对所有程序集中的公开类以及公开类里面的公开方法添加单元测试。 第二步,对于构造函数和公共属性进行单元测试。 第三步,添加全面单元测试。在产品全面提交之前可以先完成第一步的工作,二三步可以待其他所有功能完成之后再进行添加。由于第二三步的添加工作其实于第一步类似,只是在量上的累加,因此我们先着重讨论第一步的情况。 在作第一步单元测试添加的时候,也需要有选择性的进行,我们要抓住重点进行测试。首先应该针对属于框架技术中的代码添加单元测试。这里就包含操作数据库的组件、操作外部WebService的组件、邮件接收发送组件、后台服务与前提程序之间的消息传递的组件等等。通过为这些主要的可复用代码进行测试,可以大大加强底层操作的正确性和健壮性。 其次为业务逻辑层对界面公开的方法添加单元测试。这样可以让业务逻辑保持正确,并且能够将大部分的业务操作都归纳到单元测试中,保证以后产品发布之后,一旦出现问题可以直接通过业务逻辑的单元测试来找到BUG。剩下的代码大部分属于代码生成器生成的,而且大多数的操作都是类似的,因此我们可以先针对某一个业务逻辑对象做详细的单元测试。通过这样的规定,单元测试添加的范围就减少了很多。 如果项目是刚刚开始的,那么应当对所有公开的方法和属性都添加单元测试。
? 3.2 单元测试代码的写法
在编写单元测试代码的时候需要认真的考虑以下几个方面:
l
、所测试的方法的代码覆盖率必须达到100%。
2、
所测试的代码内部的状态,例如执行了某个方法之后,该方法所在的类中某个属性或者返回值是否与预期相同。
3、
被测试代码所使用的外部设备的状态,如数据库是否可读、网络是否可用、打印机是否可用、WebService是否可用等等。每一段单元测试代码,必须考虑到以上的三个问题,并且对于这些问题都要有相应的测试。
3.2.1 代码覆盖率要求
在2.5小节中已经讲了什么是代码覆盖率和代码覆盖率查看的方法。在这里我们着重来讲怎样将代码覆盖率提升到100%。
一般情况下,代码覆盖率低,说明测试代码中没有过多的考虑某些特殊情况。特殊情况包括:
l 边界条件数据,比如值类型数据的最大值、最小值、DBNull,或者是方法中所使用的条件边界,例如a>100那么100就变成了这个数据的边界。而且在测试的时候还必须把超出边界的数据作为测试条件进行测试。
2 空数据,一般空数据对应于引用类型的数据,也就是Null值。
3 格式不正确数据,对于引用类型的数据或者结构对象,类型虽然正确但是其内部的数据结构不正确的数据。例如一个数据库实体对象,数据库中要求其某个属性必须为非空,但是这时我们可以属于一个空。这样这个对象就属于一个不正确数据库。这三种数据都是针对被测试方法中所使用的外部数据来说的。方法中使用的外部数据无非就是方法参数传入的数据和方法所在的对象的属性或者字段的数据。因此在编写测试代码的时候就必须将这些使用到的数据设置为上面这几种情况的数据来检测方法执行的情况。这才能保证方法编写是正确的。
在编写单元测试代码的时候先了解到被测试方法可能会使用的外部数据,然后将这些外部数据一次设置为上面规定的这几种情况,然后再执行方法。这样就基本可以达到外部数据所有情况都能够正确测试到了。通过这种方法编写的单元测试代码覆盖率一般可以超过80%。
3.2.2 预期值是否达到
在编写单元测试的时候,不能单纯的追求代码覆盖率。有时候代码覆盖率已经达到了100%,程序也能正常运行,但是可能会出现方法执行完毕之后某些数据并非预期的数值。这时就必须对执行的结果进行断言。在.NET提供的单元测试模块中,可以在单元测试中直接使用一个类的一些静态方法来判断某个值是否达到了预期的情况。这个类是Assert。在这个类中公开了很多判断等效性、判断开关性、判断非空性等一系列方法。这些方法可以让你提前做出预测,一旦程序执行之后,如果这些断言不能通过,就代表代码有错误。
在使用断言的时候,我们要求要达到平均5行测试代码就要有一个断言。
通过添加断言,我们就可以对程序执行过程中数据的正确性做一个检测,保证我们的程序不出现写错数据的情况或者出现错误状态的情况。
3.2.3 外部设备状态更改时测试是否正常通过
当代码覆盖率和预期值都达到了我们的要求之后,整个程序其实就基本达到了质量标准。但是这样还不全面,因为很多程序都要使用到外部的设备或者程序,例如数据库、打印机、网络、串行口、并行口等等。当这些设备发生改变或者不可用的时候,程序就可能出现一些不可预知的错误。因此一个健壮的程序也必须考虑到这些情况,这时通常都是通过将这些设备确定的设置为这些不正常状态来检测程序可能会出现的问题。然后再在测试程序中将这些条件加上。 上面所介绍的只是简单的单元测试的入门级别的要求,当然真正的单元测试还有很多更加复杂的要求和测试技巧。但是对于一个初学者而言,如果能达到上述的要求,那么你的代码的健壮性应该能够满足大部分要求了。只是在比较标准的工业化开发的时候,才需要将单元测试继续深化。如果有时间的话,我会在以后再详细阐述较深层次的单元测试方法。