《单元测试的艺术》读书笔记----测试代码的最佳实践
- 测试层次和组织
1、在自动化每日构建中运行单元测试和集成测试,如使用持续集成工具自动化构建;
2、基于速度和类型布局测试:
根据运行测试所花费的时间很容易就能区分集成测试和单元测试,把集成和单元测试分开放置,放在不同的目录,指定单元测试和集成测试运行的频率。
3、确保测试时源代码管理的一部分,共同放在版本管理器进行管理。
4、将测试类映射到被测试代码
创建测试类时,应该怎样组织和放置它们呢?我们希望可以找到一个项目的所有相关测试,一个类的所有相关测试,一个方法的所有相关测试。我们可以采用以下方式:
(1)测试类和被测试类放到同一个项目内;
(2)测试类和被测试类尽量保持相同或相似的包层次;
(3)针对同一个被测试方法的多个测试方法命名,可以采用如userLoginTest_Success,userLoginTest_Fail等。
5、构建测试API,如使用测试类继承模式,创建测试工具类等;
- 优秀单元测试的支柱
1、编写可靠的测试
(1)决定何时删除或修改测试
单元测试何时会执行失败?
产品缺陷,不必修改测试,只需修复产品缺陷;
测试缺陷,需要修复测试;
产品语义或API变更,使用方式改变了,需要修改测试;
重命名含义不清的测试,重构不可读的测试。
删除重复测试。
(2)避免测试中的逻辑
如果单元测试包含了switch、if、else、foreach、for、while等语句就说明你的测试里包含了不应有的逻辑。
如果需要复杂大型测试,如多线程测试,你应该在标明为集成测试的包里编写这种测试。
如下测试代码也包含了不应有的逻辑,无意中重复了产品代码的逻辑user + greeting,
1 public void addString(){ 2 String user = "USER"; 3 String greeting = "GREETING"; 4 String actual = MessageBuilder.Build(user, greeting); 5 6 assertEqual(user + greeting, actual); 7 }
改成如下代码就消除了引入逻辑:
1 public void addString(){ 2 String user = "USER"; 3 String greeting = "GREETING"; 4 String actual = MessageBuilder.Build(user, greeting); 5 6 assertEqual("USER GREETING", actual); 7 }
(3)只测试一个关注点
一个测试方法里保持只有一个断言,我们就更容易诊断出了什么问题。
(4)把单元测试和集成测试分开
单元测试很容易运行,集成测试很可能失败,如果不够稳定,开发人员就会跳过所有测试,无法发挥单元测试的作用。
(5)用代码审查确保代码覆盖率
如果没有做代码审查,代码覆盖率统计的结论没有说服力。因为开发人员可能在测试方法里不写一个断言,测试总能通过。
代码审核有助于提升团队的技术水平,还可以创造出可读、高质量、能够持续使用多年的代码,并使你充满自信。
2、编写可维护的测试
(1)测试私有或受保护的方法
使方法成为公共方法;
把方法抽取到新类;
使方法成为静态方法。
(2)去除重复代码
抽取辅助方法去除重复代码。
使用@Before或者@After去除重复代码;
(3)已可维护的方法使用@Before
局限性:
@Before方法只用于需要进行初始化工作时;
@Before方法应该只包含适用于当前测试类中所有测试的代码,否则这个方法会更难以阅读和理解。
尽量不用@Before方法,而封装辅助初始化方法,每个测试方法手动调用。这样增加代码可读性。
(4)实施测试隔离
定义:一个测试应该总是能独立运行,不依赖于任何其他测试。
测试隔离的臭味道:
强制的测试顺序:测试需要特定的顺序执行,或者来自其他测试结果的信息;
隐藏的测试调用:测试调用其他测试;
共享状态损坏:测试共享内存里的状态,却没有回滚状态;
外部共享状态损坏:集成测试共享资源,却没有回滚资源;
(5)避免对不同关注点多次断言
1 @Test 2 public void CheckVariousUsmResult(){ 3 assertEqual(3, sum(1001, 1, 2)); 4 assertEqual(3, sum(1, 1001, 2)); 5 assertEqual(3, sum(1, 2, 1001)); 6 }
以上单元测试使用了三个简单的断言,进行了三个不同的子功能测试,希望能节省一些时间。这样做法有什么问题呢?如果断言失败,会抛出异常,后续的断言将得不到执行,即后续的功能得不到测试。但这种情况下,即便一个断言失败了,你还是会希望知道其他的断言结果。
你可以才起别的方式实现这个测试:
给每个断言创建一个单独的测试;
使用参数化测试(.Net支持,Java目前好像不支持);
把断言放在一个try-catch块中。
(6)对一个对象的多个状态的比较时,有两种方式:
方法一、多断言方式
1 @Test 2 public void compare(){ 3 String userName = "zhangf"; 4 String realName = "张飞"; 5 String id = "1001"; 6 User user = new User(id, userName, realName); 7 8 assertEqual(id, user.getId()); 9 assertEqual(userName, user.getUserName()); 10 assertEqual(realName, user.getRealName()); 11 }
方法二、单个断言方式,toString()比较
1 @Test 2 public void compare(){ 3 String userName = "zhangf"; 4 String realName = "张飞"; 5 String id = "1001"; 6 User user = new User(id, userName, realName); 7 assertEqual("id:"+id+",userName:"+userName+",realName:"+realName, user.toString()); 8 }
第一种方式让人看起来以为对多个功能做测试,可读性差,第二种方式可读性强。推荐第二种方式。
(7)避免过度指定
过度指定是对被测试单元如何实现其内部行为进行了假设,而不只是检查其最终行为的正确性。
主要有以下几种情况:
测试对一个被测试对象的春内部状态进行了断言;
测试使用了多个模拟对象;
测试在需要存根时使用模拟对象;
测试在不必要的情况下指定顺序或使用了精确匹配。如对返回的字符串进行精确匹配断言,而实际只需对字符串的一部分做断言就可以了,我们可以不适用String.equal(),而使用String.contains()。
3、编写可读的单元测试
(1)单元测试命名
测试方法名包括三部分:被测试方法名,测试场景,预期行为。
如测试用户登录,场景是多次登陆后要求使用验证码,预期行为是密码错误而失败,可命名为void userLogin_requirePictureNum_fail(){...}
(2)变量命名
合理的命名变量,可以确保阅读测试的人容易理解你要验证什么。因为单元测试不仅起到测试的作用,还是作为API的一种文档。
不好的命名如魔法数字,assertEqual(-100, result),无法看出-100是什么意义,将-100赋值给一个富含表达性命名的变量,如" COULD_NOT_READ_FILE = -100;",然后用变量做equal,则更容易理解断言的目的。
(3)有意义的断言
尽量不要编写自己的定制断言信息,如果必须编写,请命名清楚明白。
(4)断言和操作分离
反例:
assertEqual(COULD_NOT_READ_FILE, log.GetLineCount("aaa.txt"))
正例:
int result = log.GetLineCount("aaa.txt");
assertEqual(COULD_NOT_READ_FILE, result);
(5)@Before和@After
这两个方式经常被滥用,以至于方法完全不可读。
一种滥用的情况:在@Before中准备存根和模拟对象,导致阅读测试的人意识不到测试中使用了模拟对象,也不知道对象的预期值是什么。
如果由测试方法自己直接设置初始化模拟对象,设置所有的预期值,测试可读性会更好。
要点:测试要随着被测试系统一同成长和变化。
posted on 2018-05-08 15:36 滴水穿石,写自己的故事 阅读(318) 评论(0) 编辑 收藏 举报