本文概述
测试驱动开发
总结
单元测试
单元测试是软件测试中最基本最重要的测试,单元测试针对的是程序的最基本单元:函数、方法或过程。假设开发一个简单模块,其中包含若干的类,每个类中包含若干的函数,那么我们在开发的时候可以对每个类逐个实现,实现的时候对每个类中的函数逐个进行测试。
单元测试使用白盒测试,常见测试方法有:代码检查法、静态结构分析、静态质量度量、逻辑覆盖、基本路径测试、域测试、符号测试、Z路径覆盖和程序变异。
其中单元测试中常见的测试方法是路径覆盖,先将程序的执行流程使用一个流图表示,其中程序从开始到结束为一条路径,路径覆盖测试要求我们对每条路径都至少使用一个用例进行覆盖,具体可以参考我之前的文章:软件开发测试:单元测试和路径覆盖测试详细指南。
单元测试使用的工具有xUnit系列的测试框架,例如Java中的JUnit,Python中的PyUnit,或C中的CUnit。
另外,还可能用到mock框架,例如Mockito,主要是用来mock一些对象,而不必使用生产环境中的真实对象,可以实现解耦,如果测试都用真实数据,那么全部运行测试的时候,恐怕会污染真实数据;另外要注意的是数据库的测试,一个可行的方法是在测试的时候通过控制事务实现操作回滚,而不至于污染真实的数据。
单元测试的内容还是非常丰富的,不管如何,我认为至少还是要会路径覆盖测试和一个测试框架的基本使用,而其它则是靠自己的额外学习和使用,要学完似乎是不可能的,不然就老了,技术学的一半一半,钱又没赚到,女朋友又没有。。。。。。
测试驱动开发
测试驱动开发简称TDD,是软件开发的一个重要内容。我以前刚学编程的时候根本不知道这个东西有什么用,看教程似懂非懂,后面明白了:是因为年龄还不够。每次我开发一些东西,首先大概设计一番,然后要什么类或接口直接写,然后具体实现,后来学会了用单元测试测试一下,但是这时问题来了,我发现我这样写代码的思想是一种过程式编程,就是要实现A,那么将实现步骤细化,每个子功能用函数封装,直到实现A,虽然过程中可能会用到抽象类或接口进行扩展,但这还是属于过程式编程。
并且这样做有一个问题:实现A,有N个函数负责实现,测试的时候往往很难对单个函数进行测试,因为这N个函数可能会共享一个数据结构或数据对象,或者一个函数依赖于上一个函数的结果……怎么说呢,就是感觉脑子被塞住了,总觉得这样测试很有问题,到后面修改实现或修复bug的时候,测试写的更凌乱了,有时改一下还其它测试可能就出错了,还要手动一一修改正确;另外每次想把代码写得类似JDK那样标准或类似提供一个优良API接口那样,似乎都写得非常不标准。虽然如此,只要数据结构和算法有一定的基地,基本你写的项目还能用吧,就是设计得不够标准。
代码和测试写多了,自然开始明白TDD到底有什么用了。过程式编程的最大问题是它是首先先考虑逻辑功能的具体实现,却没有从使用者的角度考虑。TDD就是从使用者的角度考虑,从先使用开始编程到逻辑功能的具体实现,所以这是和过程式编程相反的。
使用TDD开发,你可以写出和JDK一样标准的代码,或者提供一些标准的API接口供别人使用。
这里举个简单的例子,假设我们要实现一个简单的文件格式转换器,例如将html文档转为PDF,或将PDF文档转为HTML文档,就是N种格式的文档格式的互相转换。
那么,我们第一时间什么都不要考虑,假设这个转换器是存在的就是了,现在就直接使用它:
@Test
void testConverter() {
FileObject htmlFile = new FileObject("C:/files/index.html", FileType.HTML, "UTF-8");
assertNotNULL(htmlFile);
FileObject pdfFile = new FileObject("C:/files/index_pdf.pdf", FileType.PDF, "UTF-8");
assertNotNULL(pdfFile);
IConverter converter = new DefaultConverterImpl();
assertNotNULL(converter);
boolean isOK = converter.convert(htmlFile, pdfFile);
assertTrue(isOK);
}
假设有提供以上功能的程序是不是就很棒了?没错,TDD就是这样写代码的,这时候即使你还没有相关的类也没有关系,你可以即时创建。
- FileObject就是个简单的Java对象,创建自动生成getter和setter即可。
- IConverter是一个转换器接口,后面new的是它的默认实现,防止我们以后还有其它不同的实现。
- 直接调用convert即可实现文件格式的转换。
- 这就是这个转换器的主要核心功能了,对于其他扩展,例如转换和保存都只使用临时目录、获取转换时间、添加转换进度监控接口、转换出错回掉接口、转换而不保存、获取文件内容中间对象(一种无格式的公共对象保存文件信息)等等,你可以扩展FileObject类、重载convert函数,扩展一个功能前先写一个测试。
- 接下去写完这个代码,下一步就是具体实现convert函数,如果实现convert的过程中又要依赖其它新的类,那么仍然是按照这个方式从上而下实现:先写一个测试,写出使用它的标准调用形式,再具体实现,等实现完了,再递归回上一层,直到功能被完全实现。
- 尽可能每句代码写一个测试用例,例如new一个对象一个用例,我们知道,首先是写标准调用的代码,但是也要记得要有相关的测试函数,例如断言或假设,我们实现代码就是要保证这个测试通过,测试通过那么就表明这个功能基本实现了(作弊的例外,例如测试函数要等于1,直接返回1)。
这样写代码是不是很爽?头发又多了几根,另外要注意的是,我们需要考虑路径覆盖的问题,在实现核心功能之余也要考虑,尽可能覆盖所有执行路径,并且对每条路径的输入值范围进行考虑,例如边界极值、特殊值,做到这几点,我们基本可以保证:1)功能已经完全实现;2)有完整的路径覆盖,这个程序基本有80%的运行正确率了。
单元测试是对一个功能的基本单元进行测试,理论上说,当这些单元测试都通过,意味着所有这些功能组合起来都是没问题的——但这只是理论,我们仍旧需要将这些功能组装起来进行测试,这就是集成测试,集成测试又叫做组装测试,它负责将各个模块或功能组装起来进行完整的测试。
集成测试可用的工具有:
Rational Integration Tester
TESSY
Protractor
Steam
集成测试不属于黑盒或白盒测试,个人认为它是一种比较笼统的测试,例如上面文件格式转换器的例子,这段代码就是集成测试了,但是与系统测试相比,集成测试还只是简单的几个功能或模块间的组合测试,基本就是测试一个或几个功能之间组合运行起来是否没问题。
如果你经常写单元测试,那么自然也会写到集成测试的代码,我们还是会想知道这几个功能组合起来是否正确,因为它们还是要被组合用到实际代码汇总,用之前先测试一下是很有必要的。
说它比较笼统是因为,要覆盖什么似乎不明确,一个例子是:访问数据库中的数据并调用第三方HTTP API接口,在这个场景中,如果要实现一个严格的集成测试,我们需要从上而下或从下而上进行。从上而下的意思是,先整体测试,然后降低粒度,对其中的子功能进行测试;从下而上则是相反,首先单元测试是通过的,然后通过不断复合进行测试——但是这样还是比较笼统,这只能测试这样用是否有问题,并不是一种详细的测试(比如单元测试)——但这样已经够了,毕竟每种测试都不是完美的,达到它的目的就好。
系统测试
系统测试是对整个系统的测试,和集成测试有一部类似,例如将程序的所有功能都组合起来进行测试。另外它需要考虑整个计算机环境:包括硬件和软件环境。
这种测试,我们可以依据详细的项目需求、网络、内存、硬盘等因素进行测试,但是仍然是不够准确的。相对来说,我认为进行集成测试后,直接进行黑盒测试或者会不用浪费那么多时间,因为这些测试要求在黑盒中同样有,黑盒测试也可以针对需求功能和环境进行测试。
UI测试对用户界面进行测试,包括用户界面和用户体现,对于一个喜欢写无界面程序的程序员来说,UI测试真的挺烦的,例如设计一个好的界面,也得调来调去。
但UI测试仍然是一种模糊的测试,测试的依据无非又是需求文档,文档怎么写的就怎么检查。测试的方法有:静态测试,通过静态分析和查看测试,另一个是动态测试,手动一个个点击,一个个体验——多么无技术含量,但是还是要做对不对?——这不,现在有自动化测试了,除了检查用户体现,关于实际操作的测试我们可以让机器自动化完成。
自动化测试
自动化测试一般是针对UI层面的测试,也就是说有用户操作界面的那些程序。自动化测试常用的框架有selenium,实际上自动化测试非常强大,实现自动化,你可以省下很多时间吃多一份外卖——关键是要有钱,而这些框架有时并不只用于测试,例如网络爬虫中也可以会用到,实际上软件自动化技术多少都可能会用到这些框架。
一般实现自动化测试,要求你需要有一些编程的基础,例如编写一些脚本,或者使用一些框架进行编程实现测试。
总结
总的来说,在软件开发中,单元测试是必须的——可能有的人不习惯写单元测试,写完就算了,实际上,很多问题类似黑盒测试的方法都很难检测多,尽量写多一些单元测试可以使你写出的程序更加健壮。
另外一个重要的技术就是自动化测试,其相关框架可以实现很多自动化的东西,即使你不喜欢自动化测试,我也建议你学一下相关工具或框架,很多东西增加自动化的处理可以让你省去很多时间。