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

  • 测试层次和组织

        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编辑  收藏  举报

导航