《单元测试的艺术》读书笔记----测试代码的最佳实践

  • 测试层次和组织

        一、在自动化每日构建中运行单元测试和集成测试,如使用持续集成工具自动化构建;多线程

        二、基于速度和类型布局测试:工具

        根据运行测试所花费的时间很容易就能区分集成测试和单元测试,把集成和单元测试分开放置,放在不一样的目录,指定单元测试和集成测试运行的频率。布局

        三、确保测试时源代码管理的一部分,共同放在版本管理器进行管理。单元测试

        四、将测试类映射到被测试代码测试

        建立测试类时,应该怎样组织和放置它们呢?咱们但愿能够找到一个项目的全部相关测试,一个类的全部相关测试,一个方法的全部相关测试。咱们能够采用如下方式:ui

        (1)测试类和被测试类放到同一个项目内;spa

        (2)测试类和被测试类尽可能保持相同或类似的包层次;线程

        (3)针对同一个被测试方法的多个测试方法命名,能够采用如userLoginTest_Success,userLoginTest_Fail等。code

       五、构建测试API,如使用测试类继承模式,建立测试工具类等;对象

 

  • 优秀单元测试的支柱

        一、编写可靠的测试

      (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)用代码审查确保代码覆盖率

        若是没有作代码审查,代码覆盖率统计的结论没有说服力。由于开发人员可能在测试方法里不写一个断言,测试总能经过。

        代码审核有助于提高团队的技术水平,还能够创造出可读、高质量、可以持续使用多年的代码,并使你充满自信。

 

        二、编写可维护的测试

        (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、多断言方式        

 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 }

 

         方法2、单个断言方式,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()。

 

        三、编写可读的单元测试

        (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中准备存根和模拟对象,致使阅读测试的人意识不到测试中使用了模拟对象,也不知道对象的预期值是什么。

        若是由测试方法本身直接设置初始化模拟对象,设置全部的预期值,测试可读性会更好。

 

        要点:测试要随着被测试系统一同成长和变化。

相关文章
相关标签/搜索