有效的单元测试

前言

对之前的项目进行重构,由于之前的项目中的单元测试大部分都是走走形式,对单元测试疏于管理,运行之后大部分是不通过,这样的单元对项目而言毫无价值,更不要说有助于理解系统功能。这也使我有契机了解到BBD(测试驱动开发)的思想。为了在项目重构中编写有效的单元测试,我查找了有关BBD的一些书籍,《单元测试的艺术》(Roy Osherove著)和《有效的单元测试》(科斯凯拉著)都是有关测试驱动开发的不错的书籍,前者是使用.net语言,后者使用java语言,作为java程序员我自然选择了后者。但实际上作者在阐述一种思想,不论哪种语言都可以读懂,只是平时的习惯,对于熟悉的语言读起来更顺畅。这篇文章也是对书中的内容做一个总结。

一、单元测试代码的可读性

①使用更易懂的API,把你的代码读出来

示例:

//代码一
String msg = “hello,World”;
assertTrue(msg.indexOf(“World”)!=-1);
//代码二
String msg = “hello,World”;
assertThat(msg.contains(“World”),equals(true));

同样断言字符串中包含 World 这个单词,代码一中 使用indexOf 这个取得单词索引位置的API就显得间接许多,而且我们的大脑还需要对表达式进行判断,进一步增加了认知的负担,而contains 方法字面意思就是包含,更符合我们要表达的意思。所以一定要找到更适合易懂的API。同时用assertThat方法替代assertTrue方法,使的整个语句更具口语化,完全可以像读文章一样读出来

②避免使用较底层的方式,比如位运算符(这并不能表示你有多牛 =.=)

示例:

//代码一
public class PlatformTest {
    @Test
    public void platformBitLength(){
        assertTrue(Platform.IS_32_BIT ^ Platform.IS_64_BIT);
    }
}
//代码二
public class PlatformTest {
    @Test
    public void platformBitLength() {
        assertTrue("Not 32 or 64-bit platform?", Platform.IS_32_BIT || Platform.IS_32_BIT);
        assertFalse("can‘t be 32 and 64-bit at the same time.",Platform.IS_32_BIT && Platform.IS_32_BIT);
    }
}

代码一 要检查的是什么?位运算符结果怎么算?恐怕大部分使用高级语言的程序员很少会用到,这会增加我们的认知负担。

位运算符可能会有效的执行一个程序,但单元测试的代码可读性优于性能,我们应该更好的表达我们的意图,使用布尔运算符来替换位运算符可以更好的表达意图,见示例二。

③不要在测试中对代码进行过度运用防御性策略

1 public void count(){
2         Data data = project.getData();
3         assertNotNull(data);
4         assertEquals(4,data.count());
5 } 

第三行代码有些画蛇添足,即使data为空,在没有第三行代码的情况下,测试案例依然会失败,在IDE中双击失败信息,可以快速跳转到失败行,并指出失败原因。所以第三行代码并没有意义,这种防御性策略的真正优势在于方法链中抛出空指针的时候。比如 assertEquals(4,data.getSummary().getTotal()),当此行代码抛出空指针异常时,你无法判断是data为空还是data.getSummary()为空,此时可以先进行assertNotNull(data)的断言。

二、单元测试代码的可维护性

①去除重复,包括结构性重复

 1 //代码一
 2 public class TemplateTest(){
 3      @Test
 4     public void emptyTemplate() throws Exception{
 5         String template=“”;
 6         assertEquals(template,new Template(template).getType());
 7    }
 8     @Test
 9     public void plainTemplate() throws Exception{
10         String template=“plaintext”;
11         assertEquals(template,new Template(template).getType());
12   }
13 }

两个测试方法,一个是测试建立一个空模板,另一个测试建立一个纯文本模板,明显可以发现存在结构性重复,对以上代码进行改进,如下:

 1 //代码二
 2 public class TemplateTest(){
 3      @Test
 4     public void emptyTemplate() throws Exception{
 5         assertTemplateType(“”);
 6     }
 7     @Test
 8     public void plainTemplate() throws Exception{
 9         assertTemplateType(“plaintext”);
10     }
11    private void assertTemplateType(String template){
12       assertEquals(template,newTemplate(template).getType())
13    }
14 }

虽然代码行数没有减少,甚至还多了一行,但是把相同的代码提炼到一处,当它发生变动时只需修改一处,可维护性增强了。

②避免由于条件逻辑而造成的测试遗漏,存在条件逻辑时要在最后加上 fail()方法,强制测试失败

考虑一下,当Iterator 为空的时候,下面的测试方法会失败吗?

 1 //重构前
 2 public class DictionaryTest{
 3 @Test
 4 public void testDictionary() throws Exception{
 5     Dictionary dict = new Dictionary();
 6     dict.add(“A”,new Long(3));
 7     dict.add(“B”,”21”);
 8     for(Iterator e = dict.iterator();e.hasNext()){
 9         Map.Entry entry = (Map.Entry) e.next();
10         if(“A”.equals(entry.getKey()))
11             asserEquals(3L,entry.getValue());
12         if(“B”.equals(entry.getKey()))
13             assertEquals(“21”),entry.getValue();
14      }
15   }
16 }

显然当Iterator为空时,测试并不会失败,这并不符合我们单元测试的目的,进行重构后:

 1 //重构后
 2 public class DictionaryTest{
 3 @Test
 4 public void testDictionary() throws Exception{
 5     Dictionary dict = new Dictionary();
 6     dict.add(“A”,new Long(3));
 7     dict.add(“B”,”21”);
 8     assertContain(dict.iterator(),”A”,3L);
 9         assertContain(dict.iterator(),”B”,21);
10   }
11 private void assertContain(Iterator i,Object key,Object value){
12         while(i.hasNext()){
13             Map.Entry entry = (Map.Entry)i.next();
14             if(key.equals(entry.getKey())){
15                 assertEquals(value,entry.getValue());
16                return;
17             }
18         }
19         fail("Iterator didn‘t contain "+ key);
20     }
21 }

当没有达到预期目的时使用 fail()方法,强制测试失败。

③避免使用sleep方法浪费大量的测试时间

counterAccessFromMultipleThreads 用来测试一个多线程计数器,开启10个线程,每个线程调用计数器1000次,sleep(500),是为了让主线程等待开启的10个线程执行完毕

那么问题来了,如果在10毫秒内所有线程都执行完毕,岂不白白浪费了490毫秒?又或者在等待500毫秒后仍有线程没有执行完毕,那该怎么办?

 1 @Test
 2 public class counterAccessFromMultipleThreads{
 3   final Counter counter = new Counter();
 4   final int callsPerThread = 1000;//每个线程调用计数器1000次
 5   final Set<Long> values = new HashSet<Long>();
 6   Runnable runnable = new Runnable(){
 7       public void run(){
 8           for(int i=0;i<callsPerThread;i++){
 9               values.add(counter.getAndIncrement());
10           }
11       }
12   };
13   int threads = 10;//开启10个线程
14   for(int i=0;i<threads;i++){
15       new Thread(runnable).start();
16   }
17   Thread.sleep(500);
18   int exceptedNoOfValues = threads * callsPerThread;
19   assertEquals(exceptedNoOfValues ,values.size());
20 }

改进后的测试方法:

 1 public class counterAccessFromMultipleThreads{
 2   final Counter counter = new Counter();
 3   final int callsPerThread = 1000;
 4   final int numberOfthreads = 10;
 5   final CountDownLatch allThreadsComplete = new CountDownLatch(numberOfthreads);
 6   final Set<Long> values = new HashSet<Long>();
 7   Runnable runnable = new Runnable(){
 8       public void run(){
 9           for(int i=0;i<callsPerThread;i++){
10               values.add(counter.getAndIncrement());
11           }
12           allThreadsComplete.countDown();
13       }
14   };
15
16 for(int i=0;i<numberOfthreads;i++){
17       new Thread(runnable).start();
18   }
19   allThreadsComplete.await();
20   //  allThreadsComplete.await(10,TimeUnit.SECONDS);
21   int exceptedNoOfValues = threads * callsPerThread;
22   assertEquals(exceptedNoOfValues ,values.size());
23 }

  等待所有线程结束后再继续执行,有更好的办法,java.util.concurrent 包中的CountDownLatch类完全可以胜任这项工作。

  调用await方法开始阻塞,直到所有的线程都通知完成,然后继续执行主线程代码。也可以设置超时时间,allThreadsComplete.await(10,TimeUnit.SECONDS); 如果10秒钟内子线程仍未执行结束,也会继续执行主线程。

三、单元测试代码的可维护性

①避免歧义注释

 1  /**
 2      * 功能描述: 发送邮件<br>
 3      * 〈功能详细描述〉
 4      * @return
 5      * @see [相关类/方法](可选)
 6      * @since [产品/模块版本](可选)
 7      */
 8     9   public void sendShortMessage() {
10       //todo11    }    

  有时候有注释,不如无注释。 可以看到以上代码的注释为发送邮件, 但方法名却为sendShortMessage ,明显为发送短信的意思。这时候我们可能就会想这段代码是要发送邮件还是要发送短信,为了弄清事实不得不去看方法体的内容。 造成这种歧义注释的原因很多,可能之一就是发送短信的方法大致流程可能跟发送邮件相近,所以直接拷贝了邮件的代码,改了方法的内容,却没有修改注释。如果方法名足够得当,可以不写注释。

②避免永不失败的测试

下面的测试代码检查是否抛出期望的异常,这段代码有什么问题?

@Test
public void includeForMissingResourceFails()
    try{
        new Environment().include("somethingthatdoesnotexist");
       }catch(IOException e){
        assertThat(e.getMesssage(),contians(“FileNotExist”));
}

上面的代码测试结果如下:

1.如果代码如期工作并抛出异常,异常会被catch代码块捕获,测试通过。

2.如果代码没有如期工作,也就是没有抛出异常,则方法返回,测试通过,我们并不会发现其中存在的问题。

改进测试方法:

1 try{
2         new Environment().include(“FileNotEixst”);
3         fail();
4    }catch(IOException e){
5    assertThat(e.getMesssage(),contians(“FileNotExist”))}

添加fail()方法的调用,使测试起作用。除非抛出期望的异常,否则测试失败。

四、优秀的单元测试的原则

    •少用继承多用组合,继承更大程度上是为了多态而非复用代码

    •单元测试应该模块化,每个模块小而专注,减少反馈链

    •如果一个单元测试方法失败了,那么导致它失败的原因只有一个

    •加载外部文件时使用相对路径而不是绝对路径

    •对于魔法数字除了提取局部变量或常量外,可以取一个恰当的方法名,见名知义

    •好的注释应解释代码现状的缘由

  可以看出优秀的单元测试的原则跟优秀的面向对象编程的原则一致,比如少用继承,多用组合,模块化且模块尽可能小,一个模块只完成一个功能等。

五、BBD测试驱动开发

 

  测试驱动开发流程如上图:在开发前先写一个失败的测试案例,然后写出使测试代码通过的生产代码,重构优化生产代码和测试代码直至通过测试,然后再写一个新的测试,循环上述过程。

  当你的生产代码写的一团糟的时候,你很难,甚至是不可能按照优秀单元测试的原则去编写测试代码。比如一个测试方法要求只测试一件事情,而当生产代码一个方法干了很多的事情,测试方法很难保证只测试一件事情,这时候只能重构生产代码才能写出优秀的测试

  究其根本,测试驱动开发的本质是,当你的测试代码符合模块化、松耦合高内聚的特点时,生产代码会自然的“被逼迫”遵守同样的原则,从而产生良好的设计。

时间: 2024-10-20 00:52:00

有效的单元测试的相关文章

单元测试Junit

###<center> 单元测试Junit </center>###- - -1.**单元测试**:> ==单元测试==是软件之中对于最小的功能模块的的测试,其可以对最基本的软件构成单元来测试.> 需要注意的是:> >**测试用例是用来达到测试想要的预期结果,而不能测试出程序的逻辑错误**. 2.**JUnit**:>1.**Junit是基于断言机制的**.是用于编写可复用测试集的简单框架,是xUnit的一个子集.xUnit是一套基于测试驱动开发的测试

MVC与单元测试实践之健身网站(四)-动作管理

网站后台负责进行动作的管理,包括动作名称.介绍.训练要点.配图等内容,以便前台能够使用这些内容.在上一篇< Fit项目图片上传和云存储的调通>中已经准备好了这里涉及到的主要技术难点,现在就开始完成该模块了. 一 列表介绍 健身管理模块包括肌群.肌肉的显示以及动作的管理.这儿也算是开始涉及"业务内容"了,还好我之前有储备了一些关于健身的资料,现在是时候派上另一种用场了. a) 肌群和肌肉因为内容相对固定,所以为了减少业务逻辑以及单元测试的代码量,当然最主要是为了偷懒,就只提供

MVC与单元测试实践之健身网站(二)-管理员模块

开始动手做这个项目时,发现无法做到完全的先设计.再编码,于是决定分模块进行,从管理员模块开始设计.编码,而且接口就已经改了好几次了. 管理员模块涉及的功能有登录和后台对管理员的维护,其中也涉及前端的开发.UI模板使用Inspinia,感觉这套模板功能丰富.界面美观,而且基于HTML5和BootStrap,对这两方面的知识也可以多些了解. 在上一篇<如何在单元测试时隔离ORM>中,解决了对Service层进行测试怎样构建伪对象的问题,随后管理员模块的Service层和单元测试在齐头并进中完成了:

单元测试(一)-NUnit基础

单元测试作为提高代码和软件质量的有效途径,其重要性和益处自不必多说,虽然我没有实践过TDD之类,但坚信单元测试的积极作用.作为一种开发方法,单元测试早在上世纪70年代就已经在Smalltalk语言被运用了,这么多年来,单元测试一次又一次证明了自身的价值,在各种开发方式此起彼伏的浪潮中,经受住了时间的考验. 现在,俺也开始学习了,并在以后好好实践.这个系列的学习素材为Roy Osherove所著The Art of Unit Testing with examples in C#, 2nd Edi

OA项目CRUD和单元测试(一)

使用ModeFirst方法生成数据库,EntityFramework5.0. 一:Model层的模型:(根据模型生成数据库) 二:Dal层的UserInfo代码: namespace SunOA.EFDAL { public class UserInfoDal { //crud DataModelContainer db = new DataModelContainer(); public UserInfo GetUserInfoById(int id) { return db.UserInfo

词频统计-单元测试

我自己的单元测试没有弄出来,我用c编的,在visual studio中貌似实现不了单元测试,而李俞寰同学是用c#编写的词频统计,在vs2015中实现单元测试无比的方便,所以我请教了他并借鉴了一下. [TestMethod()] public void DictionarySortTest() { Dictionary<string,int>input=new Dictionary<string,int>() { {"you,1}, {"are",1},

使用Xunit来进行单元测试

不管你爱与不爱,单元测试对于一个软件的长治久安还是必不可少的一环.在Visual Studio 2012后,VS中的测试浏览器也能与第三方的集成了,用起来还是非常方便的.目前在.Net框架下的测试工具主要有Nunit.内置的MSTest以及Xunit这三个工具,本文就简单的介绍一下如何在VS中使用XUnit这个测试框架的后起之秀. 安装Xunit: Xunit的安装现在不需要插件支持了,直接使用NuGet安装如下两个库即可: PM> Install-Package xunit PM> Inst

作业八——单元测试练习(个人练习)

必做一: 针对附录1给出的三角形判断Java 代码,应用等价类划分法设计测试用例,用表格形式列出设计的测试用例: 测试用例如下:(红色字体为错误预言) 序号 测试输入:三条边 测试预言:[Oracle:Illegal(非三角形),Scalene(一般三角形), Isoceles(等腰三角形),Regular(等边三角形)] 1 (5,5,5) Regular 2 (-5,-5,-5) Regular 3 (1,4,5) Illegal 4 (2,3,5) Illegal 5 (3,4,5) Sc

作业8:单元测试练习(个人练习)

要求 [必做题1] 针对附录1给出的三角形判断Java 代码,应用等价类划分法设计测试用例,用表格形式列出设计的测试用例,写到博客中.(10分) [必做题2] 模仿附录2给出的三角形判断Junit测试代码,设计单元测试脚本,测试 [必做题1]设计得到的测试用例.注意测试脚本中测试用例出现顺序与[必做题1]表格所列顺序一致.运行所得的测试脚本,截运行结果图,写到博客中,同时将源代码push到你自己的github.(20分) [必做题3] 心得体会.写下本次练习你收获的知识点(PS:测试用例设计方法

实验二 单元测试

1. 学习单元测试和代码覆盖率工具的使用 (1)写一个程序,用于分析一个字符串中各个单词出现的频率,并将单词和它出现的频率输出显示.(单词之间用空格隔开,如“Hello World My First Unit Test”): (2)编写单元测试进行测试: (3)用ElcEmma查看代码覆盖率,要求覆盖率达到100%. import java.util.HashMap; import java.util.Map; public class Test { private static String