单元测试的艺术
2022-08-08 22:08:28 0 举报
AI智能生成
单元测试比较详细的总结,包括了什么是单测,怎么写单测,什么是好的单测等内容
作者其他创作
大纲/内容
什么是好的测试?(A-TRIP)
自动化(Automatic)
彻底的(Thorough)
测试了所有可能会出问题的情况
可重复(Repeatable)
每个测试独立于其他的测试和环境
独立的(Independent)
专业的(Professional)
在测试代码中,针对好设计的所有普遍规则--维护封装,采用DRY原则,降低耦和 都必须在测试代码中得到遵循
对测试进行测试
对代码进行测试,从而确信它正常工作是一个伟大的想法,但是你必须编写代码来执行这些测试,但如果在测试代码中有bug呢?
方法
如何修正bug
验明bug
编写一个将失败的测试来证明bug的存在
3.修正代码,让测试通过
4.验证所有的测试仍然可以通过
!相同的问题还会在其他地方发生吗?
不要只修复一个bug,而是捕捉到了一类bug
自设圈套
不能确信测试是否正确的话,可以让产品代码产生你想要检测的那个bug,然后验证测试是否如预期一般失败
测试哪些内容?(Right-BICEP)
结果是否正确(Right)
边界条件(CORRECT)
一致性(Conformance)
值是否和预期的一致
比如邮件地址: name@somewhere.com
如果写程序需要从邮件中提取名字,以@为分割符,如果输入的邮件地址没有@,代码怎么工作?
顺序性(Ordering)
值是否如预期的那样,是有序或者无序的
区间性(Range)
值是否位于合理的最大值和最小值之间
比如用整数表示人的岁数,但是没人能活到1000岁
比如用整数来存储角度,但是一个圆中,角度值不会大于360°
引用/耦和性(Reference)
代码是否引用了一些不在代码本身控制范围内的外部资源
如果对于类的状态,其他对象的状态,或者全局应用程序的状态,你需要做一些假设,那么你就需要对代码进行测试,保证其在假设未满足的情况下运行良好
比如汽车传动器的状态(是否切换到停车档)取决于汽车的状态(处于移动状态还是停车状态)
存在性(Existence)
值是否存在(比如,是否是非null,非0等)
基数性(Cardinatity)
这里的基数主要强调的是计数,偶发性的计数错误会非常严重。所以必须要检查测试函数是否能正确计数,并且检查最后的计数值
0-1-n原则
比如12米的一条路,每隔3m安装一个路灯,总共需要多少个路灯,路的两端都需要安装
如果想都没想就回答4,那么就错了,其实是需要5个路灯
时间性(Time)
相对时间(时间上的顺序)
比如login()会在logout()之前被调用等
代码中的超时问题,为了获得一个存活期较短的资源,你的函数会等待多长时间?
绝对时间(消耗的时间和钟表上的时间)
并发问题
并发访问和同步访问问题
程序是否线程安全
检查反向关联
对于一些方法,我们可以使用反向的逻辑关系来验证它们
比如可以对结果进行平方来检查一个计算平方根的函数
验证是否成功地插入了数据库,可以通过查询这条记录来验证
需要注意一些bug可能会被在两个函数中都出现的错误所掩盖,在平方根的验证中,可以用普通的两个数的乘法来验证
使用其他手段来实现交叉检查
比如一种计算可能有多种算法,在产品中我们应用效率高的算法,但是在测试中,可以使用其他的算法来交叉测试结果
强制产生错误条件
环境方面的约束(这些能通过单元测试处理?)
内存耗光
磁盘用满
时钟出问题
网络不可用或者有问题
系统过载
性能特性
辅助测试工具-JunitPerf
每晚或每隔几天运行一次就行
核心技术
伪对象
存根(stub)
stub和mock的区别
只会对mock对象进行断言,而不会对存根对象进行断言
模拟对象(mock)
伪对象(fake)
测试代码(对应《单元测试的艺术第二版》的第三部分)
优秀单元测试的支柱(第八章)
编写可靠的测试
1.决定何时删除或修改测试
2.避免测试中的逻辑
测试代码中不应该包含逻辑,包含逻辑的测试通常会一次测试多个东西,这样可读性差,也比较脆弱,可能包含隐藏的测试缺陷
测试代码中出现下列语句,就包含了不应该有的逻辑
switch,if或else语句
foreach,for或者while循环
不要将断言放在try-catch中
以下案例也该避免
见备注
3.只测试一个关注点
一个关注点是一个工作单元的一个最终结果
一个返回值
系统状态的一个改变
对第三方对象的一个调用
多个关注点的问题
命名测试困难,不可能给测试取一个恰当的名字,就会降低可读性
其中某一个断言失败时,其后的断言不会执行,可能会降低单测提供的有用信息
4.把单元测试和集成测试分开
5.用代码审查确保代码覆盖率有效
代码覆盖率100%能说明什么?也许这些测试代码连断言都没有,也就是无效测试
两个人或多个人坐在一起交谈,现场查看和修改同一段代码,传播经验等
编写可维护的测试
测试私有或受保护的方法
在测试时,你应该只考虑公共契约
私有方法不会无缘无故存在,一定存在某个公有方法调用它
如果一个私有方法值得测试,应该重构成公有的或者静态的
去除重复代码
单元测试中的重复代码和产品代码中的重复一样有害,DRY原则应该同样适用于测试代码
适用辅助方法,将测试中通过的代码进行封装
以可维护的方式适用setup方法
滥用setup方法的方式
在setup方法中初始化只在某些测试中使用的对象
setup代码冗长难懂
在setup方法中准备模拟对象和伪对象
实施测试隔离
用例之间隔离有问题的测试”臭味“
强制的测试顺序
隐藏的测试调用
测试调用其他测试
共享状态损坏
测试共享内存里的状态,却没有回滚状态
外部共享状态损坏
集成测试共享资源,却没有回滚资源
避免对不同关注点多次断言
在测试多个关注点时,必须保证所有的断言都能够运行,即使其他的断言已经失败了
Nunit框架中,一个测试方法中一个断言失败就不会继续执行之后的断言,这样会导致看不到系统的所有缺陷和影响排查失败的原因
使用参数化测试
如果是同一个关注点的多种不同情况
对象比较
在一个测试中验证同一对象的多个方面
可以创建一个用于比较的对象,设置所需的属性,然后使用一个断言来比较自己创建的对象和预期的对象
这种方法需要重写equal方法
【自我发散】其实也可以使用辅助方法,在这个辅助方法里来进行比较
如果只是为了让别人更容易理解你究竟在测试什么,并且认为这个测试是一个逻辑整体,而不是很多单独的测试
避免过度指定
过度指定的测试对一个具体的被测试单元如何实现其内部行为进行了假设,而不是只检查其最终行为的正确性
主要情况
测试对一个被测试对象的纯内部状态进行了断言
因为内部状态可能随着实现细节不断变化
测试在需要存根时使用模拟对象
比如验证存根是否得到调用,如果被测试方法中增加了一个内部调用或者改变了调用参数,就可能导致测试失败
只要最终结果正确,你的测试不应该关心什么内部方法调用或者有没有发生调用
编写可读的测试
单元测试命名
测试名的组成
被测试方法名
测试场景
预期行为
命名规范
methodUnderTest_Scenario_behavior()
【自我发散】java中可以用@DisplayName来使用中文表达
变量命名
示例
断言和操作分离
不要把断言和方法调用写在同一行里
示例
总结
测试要随着被测试系统一同成长和变化,要可靠,可维护,可读
在项目中进行测试
要求
所有的测试应该在任何时刻都能通过
不要出现如下情况:
不完整的代码
不能编译的代码
没有相应单元测试的代码
不能通过单元测试的代码
通过了自己的测试,但是会导致其他地方的其他测试失败的代码
频率
编写新的函数时
修正bug时
提交代码时
持续不断地
应该有一台专门的机器来定时或自动触发运行完整的构建和测试
测试遗留代码
对已经存在的代码,最好是优先给最有可能出问题的代码添加测试
可以用这些添加的测试来做回归测试,避免这些最有可能出问题的代码行为安全
如果团队中对单元测试不熟悉,建议从简单的单元测试开始,如果团队熟悉单元测试,可以自主选择从简单还是难的单测开始
测试与评审
测试代码也应该被评审
TDD(测试驱动开发)
传统编码方式和TDD的区别
优势
如果一个测试失败了,没有经过修改再次运行又成功了,你其实是在测试这个测试本身。
如果你预期一个测试会失败,但它却成功了,那测试本身可能有缺陷,或者测试的对象不对
如果一个测试之前失败了,你现在预期它成功,它却依然失败,那测试可能有缺陷,或者测试预期的结果不正确
步骤
编写一个会失败的测试,以证明产品中代码或者功能的缺失(因为还没有实现产品代码)
编写符合测试预期的产品代码,使测试通过
重构代码,或者编写下一个单元测试
总结
一般原则
测试任何可能失败的地方
测试任何已经失败的地方
对于新加的代码,在被证明正确之前,都可能是有问题的
提交代码之前做全局测试
要回答的问题
我如何知道代码运行是否正确呢
我要如何对它进行测试
还有哪些方面可能会发生错误
这个问题是否会在其他的地方出现呢
参考书籍
单元测试的艺术(第二版),金迎 译
单元测试之道Java版,陈伟柱,陶文 译
互联网单元测试与实践
收藏
收藏
0 条评论
下一页