一文帮你理解什么是单元测试

最近想对咱们的单元测试作一下总结,楼主在平常工做中写了很多单元测试,但有些概念和用法并无刨根问题的去追寻,研究。因而把一些不清晰的概念输入到google中来寻找答案,发现了几个不错的帖子,从中学到了东西,也发现了问题,和你们分享,若有错误,敬请指正。java

咱们所作的产品测试包括了下文所说的软件测试词汇表中的大部分,也就是“单元测试”,组件测试,系统测试,集成测试,压力测试和验收测试。开发团队成员作的或者参与的是“单元测试”,集成测试。这里的单元测试我加了引号是由于看完下面的文章,我发现咱们所作的单元测试并非严格意义上的单元测试,叫功能测试比较恰当。下文所说的功能测试遇到的问题在咱们的实际项目中也遇到了。但愿往后有机会改进。git

另外这篇帖子的标题叫作evil unit testing,这里的evil是有害的意思,可是通读这篇博客,并无讲单元测试是有害的,可能做者的意思是把功能测试看成单元测试的想法是有毒的吧。sql

好了,原文连接:数据库

http://www.javaranch.com/unit-testing.jsp服务器

 

1. 你作的是单元测试么?

我看到过至少6个公司由于他们有“单元测试(unit test)”而满脸自豪。而咱们看到的是这种“单元测试”结果会是一个麻烦。其余人讨论单元测试有多么伟大,可是它确实变得让人痛苦不堪。这种测试须要45分钟才能跑完,还有你对代码只作了一点改动,但却破坏了7个测试用例”。网络

这些家伙用的是一堆功能测试(functional test)。他们掉入了一个流行的思惟陷阱,认为只要是使用Junit来运行的测试用例,就必须是单元测试。你只须要一点点词汇量,90%的问题就都能解决。并发

2. 软件测试词汇表

  • 单元测试(unit test

可测试代码的最小的一部分。一般是一个单一的方法,不会使用其它方法或者类。很是快!上千个单元测试可以在10秒之内跑完!单元测试永远不会使用:app

  1. 数据库
  2. 一个app服务器(或者任何类型的服务器)
  3. 文件/网络 I/O或者文件系统
  4. 另外的应用
  5. 控制台(System.out,system.err等等)
  6. 日志
  7. 大多数其余类(但不包括DTO‘s,String,Integer,mock和一些其余的类)

单元测试几乎老是回归测试套件(regression suite)的一部分。框架

  • 回归测试套件(Regression Suite:

可以马上被运行的测试用例的集合。一个例子就是放在一个特定文件夹中的可以被Junit运行的全部测试用例。一个开发人员可以在一天中把一个单元测试回归套件运行20次或者他们可能一个月跑两次功能测试回归套件jsp

  • 功能测试(Functional Test:

比一个单元要大,比一个完整的组件测试要小。一般为工做在一块儿的的几个方法/函数/类。上百的测试用例容许运行几个小时。大部分功能测试是功能测试回归套件的一部分。一般由Junit来运行。

  • 集成测试(Integration Test:

测试两个或者更多的组件一块儿工做的状况。有时候是回归套件的一部分。

  • 组件测试(Component Test:

运行一个组件。常常由QA,经理,XP客户等等来执行。这种类别的测试不是回归套件的一部分,它不禁Junit来执行。

  • 组件验收测试(Component Acceptance Test C.A.T.:

做为正常流程的一部分,它是在众多人面前运行的一个组件测试。由你们共同决定这个组件是否是知足需求标准。

  • 系统测试(system Test

全部的组件在一块儿运行。

  • 系统验收测试(System Acceptance Test S.A.T.:

做为正常流程的一部分,它是在众多人面前运行的一个系统测试,由你们来共同决定这个系统是否是知足需求标准。

  • 压力测试(Stress Tests:

另一个程序加载一个组件,一些组件或者整个系统。我曾经看到过把一些小的压力测试放到回归功能测试中来进行——这是测试并发代码的一个很聪明的作法。

  • Mock:

在单元测试或者功能测试中使用的一些代码,经过使用这些代码来确保你要测试的代码不会去使用其它的产品代码(production code)。一个mock类覆盖了一个产品类中的全部public方法,它们用来插入到尝试使用产品类的地方。有时候一个mock类用来实现一个接口,它替换了用来实现一样接口的产品代码。

  • Shunt:

有点像继承(extends)产品代码的mock类,只是它的意图不是覆盖全部的方法,而只是覆盖足够的代码,因此你可以测试一些产品方法,同时mock剩余的产品方法。若是你想测试一个可能会使用I/O的类它会变得尤其有用,你的shunt可以重写I/O方法同时来测试非I/O方法。

3. 使用太多功能测试(functional test)会有麻烦

不要误解个人意思。功能测试有很大的价值。我认为一个测试良好的app将会有一个功能测试的回归套件和一个非回归功能测试的集合。一般状况下对于一磅产品代码,我都想看到两磅单元测试代码和两盎司(注:1磅=16盎司)功能测试代码。可是在太多的项目中我看到的现象是没有一丁点单元测试,却有一磅功能测试。

下面的两幅图代表了一些类的使用状况。用一些功能测试来测试这些类一块工做的状况。修复一个类的bug会破坏许多功能测试。。。

 

上面的状况我看到过屡次。其中的一个例子是一个很小的改动破坏了47个测试用例。咱们经过开会来决定这个bug是否是要被留在代码中。最后决定咱们要留足够的时间来fix全部的case。几个月过去了,事情依然糟糕。。

解决方法是使用单元测试来代替功能测试:

 

结果是这个工程变的更加灵活。

4. 功能测试认知纠错

经过只编写功能测试用例,我能够写更少的测试代码,同时测试更多的功能代码!”这是真的!可是这会以你的工程变得更加脆弱为代价。另外,若是不使用单元测试,你的应用有些地方很难被测试。同时达到最好的覆盖率和灵活性是使用功能测试和单元测试的组合,其中单元测试的比重要大,功能测试的比重要小。

个人业务逻辑是让全部的类一块工做,因此只测试一个方法是没有意义的。”我建议你单独测试全部的方法。同时我也并不建议你不使用功能测试,它们也是有价值的。

我不介意个人单元测试组件会花费几分钟来运行”可是你的团队中的其余人介意么?你的team lead介意么?你的manager呢?若是它花费几分钟而不是几秒钟,你还会在一天的时间把整个测试套件运行屡次么?在什么状况下人们根本不会运行测试?

5. 单元测试mock基础

下面是单元测试的一个简单例子,测试各类状况却不依赖其余方法。

 1 public void testLongitude()
 2 
 3     {
 4 
 5         assertEquals( "-111.44" , Normalize.longitude( "111.44w" ) );
 6 
 7         assertEquals( "-111.44" , Normalize.longitude( "111.44W" ) );
 8 
 9         assertEquals( "-111.44" , Normalize.longitude( "111.44 w" ) );
10 
11         assertEquals( "-111.44" , Normalize.longitude( "111.44 W" ) );
12 
13         assertEquals( "-111.44" , Normalize.longitude( "111.44     w" ) );
14 
15         assertEquals( "-111.44" , Normalize.longitude( "-111.44w" ) );
16 
17         assertEquals( "-111.44" , Normalize.longitude( "-111.44W" ) );
18 
19         assertEquals( "-111.44" , Normalize.longitude( "-111.44 w" ) );
20 
21         assertEquals( "-111.44" , Normalize.longitude( "-111.44 W" ) );
22 
23         assertEquals( "-111.44" , Normalize.longitude( "-111.44" ) );
24 
25         assertEquals( "-111.44" , Normalize.longitude( "111.44-" ) );
26 
27         assertEquals( "-111.44" , Normalize.longitude( "111.44 -" ) );
28 
29         assertEquals( "-111.44" , Normalize.longitude( "111.44west" ) );
30 
31         // ...
32 
33     }

 

固然,任何人都能为上面这种状况作单元测试。可是大部分业务逻辑都使用了其它业务逻辑:

 1 public class FarmServlet extends ActionServlet
 2   { 
 3         public void doAction( ServletData servletData ) throws Exception
 4         {
 5 
 6             String species = servletData.getParameter("species");
 7 
 8             String buildingID = servletData.getParameter("buildingID");
 9 
10             if ( Str.usable( species ) && Str.usable( buildingID ) )
11 
12             {
13 
14                 FarmEJBRemote remote = FarmEJBUtil.getHome().create();
15 
16                 remote.addAnimal( species , buildingID );
17 
18             }
19 
20         } 
21 
22     }

 

这里不只仅调用了其余业务逻辑,还调用了应用服务器!可能还会访问网络!上千次的调用可能会花费很多于10秒的时间。另外对EJB的修改可能会破坏我对这个方法的测试!因此咱们须要引入一个mock对象。

首先是建立mock。若是FarmEJBRemote是一个类,我将会继承(extend)它而且重写(override)它全部的方法。可是既然它是一个接口,我会编写一个新类并实现(implement)全部方法:

 1 public class MockRemote implements FarmEJBRemote
 2 
 3     {
 4 
 5         String addAnimal_species = null;
 6 
 7         String addAnimal_buildingID = null;
 8 
 9         int addAnimal_calls = 0;
10 
11         public void addAnimal( String species , String buildingID )
12 
13         {
14 
15             addAnimal_species = species ;
16 
17             addAnimal_buildingID = buildingID ;
18 
19             addAnimal_calls++;
20 
21         }
22 
23     }

 

这个类什么都没作,只是携带了单元测试和须要被测试代码之间要交互的数据。

这个类会让你感受不舒服么?应该是这样。在我刚接触它的时候有两件事情把我弄糊涂了:类的属性不是private的,而且命名上有下划线。若是你须要mock java.sql.connection。总共有40个方法! 为每一个方法的各个参数,返回值和计数都实现Getters和setters?嗯…稍微想一下…咱们把属性声明为private是为了封装,把事情是如何作的封装在内部,因而往后咱们就能够修改咱们的业务逻辑代码而不用破坏决定要进入咱们的内脏的其余代码(也就是要调用咱们的业务逻辑的代码)。但这对于mock来讲并不适用,不是么?根据定义,mock没有任何业务逻辑。进一步来讲,它没有任何东西不是从其余地方拷贝过来的。全部的mock对象都能100%在build阶段生成!..因此虽然有时候我仍然觉的这么实现Mock有一点恶心,可是最后我会重拾自信,这是最好的方法了。只是闻起来会让你有些不舒服,可是效果比使用其它方法好多了。

如今我须要使用mock代码来替代调用应用服务器的部分。我对须要使用mock的地方作了高亮:

 1   public class FarmServlet extends ActionServlet
 2 
 3     {
 4 
 5         public void doAction( ServletData servletData ) throws Exception
 6 
 7         {
 8 
 9             String species = servletData.getParameter("species");
10 
11             String buildingID = servletData.getParameter("buildingID");
12 
13             if ( Str.usable( species ) && Str.usable( buildingID ) )
14 
15             {
16 
17                 FarmEJBRemote remote = FarmEJBUtil.getHome().create();
18 
19                 remote.addAnimal( species , buildingID );
20 
21             }
22 
23         }
24 
25     }

 首先,让咱们把这句代码从其余猛兽中分离出来:

 1  public class FarmServlet extends ActionServlet
 2 
 3     {
 4 
 5         private FarmEJBRemote getRemote()  
 7         { 
 9             return FarmEJBUtil.getHome().create(); 
11         }
12 
13         public void doAction( ServletData servletData ) throws Exception
14 
15         {
16 
17             String species = servletData.getParameter("species");
18 
19             String buildingID = servletData.getParameter("buildingID");
20 
21             if ( Str.usable( species ) && Str.usable( buildingID ) )
22 
23             {
24 
25                 FarmEJBRemote remote = getRemote();
26 
27                 remote.addAnimal( species , buildingID );
28 
29             }
30 
31         }
32 
33     }

这有一点痛..我将会继承个人产品类而后重写getRemote(),因而我能够把mock代码混入到这个操做中了。我须要作一点点改动:

 1   public class FarmServlet extends ActionServlet
 2 
 3     {
 4 
 5         FarmEJBRemote getRemote()
 6 
 7         {
 8 
 9             return FarmEJBUtil.getHome().create();
10 
11         }
12 
13  
14 
15         public void doAction( ServletData servletData ) throws Exception
16 
17         {
18 
19             String species = servletData.getParameter("species");
20 
21             String buildingID = servletData.getParameter("buildingID");
22 
23             if ( Str.usable( species ) && Str.usable( buildingID ) )
24 
25             {
26 
27                 FarmEJBRemote remote = getRemote();
28 
29                 remote.addAnimal( species , buildingID );
30 
31             }
32 
33         }
34 
35     }

 若是你是一个好的面向对象工程师,你如今应该疯了!破坏单元测试代码中的封装性是很不舒服的,可是破坏产品代码封装性的事情就不要作了!长篇大论的解释有可能帮助事态平息,个人观点是:在你的产品代码中,对类的第一次封装要永远保持警戒…可是,有时候,你可能考虑用价值20美圆的可测试性来和价值1美圆的封装性来作交易。为了让你减轻一点痛苦,你能够加一个注释:

 1 public class FarmServlet extends ActionServlet
 2 
 3     {
 4 
 5         //exposed for unit testing purposes only!
 6 
 7         FarmEJBRemote getRemote()
 8 
 9         {
10 
11             return FarmEJBUtil.getHome().create();
12 
13         }
14 
15  
16 
17         public void doAction( ServletData servletData ) throws Exception
18 
19         {
20 
21             String species = servletData.getParameter("species");
22 
23             String buildingID = servletData.getParameter("buildingID");
24 
25             if ( Str.usable( species ) && Str.usable( buildingID ) )
26 
27             {
28 
29                 FarmEJBRemote remote = getRemote();
30 
31                 remote.addAnimal( species , buildingID );
32 
33             }
34 
35         }
36 
37     }

 如今我能够实现一个类来返回mock值了:

 1  class FarmServletShunt extends FarmServlet
 2 
 3     { 
 4 
 5         FarmEJBRemote getRemote_return = null;
 6 
 7         FarmEJBRemote getRemote()
 8 
 9         {
10 
11             return getRemote_return;
12 
13         }
14 
15     }

 注意一下怪异的名字:“shunt”。我不肯定它是什么意思,但我认为这个词语来自电子工程/工艺,它指用一段电线来临时组装一个完整的电路。一开始听起来这个想法很愚蠢,可是事后我就慢慢习惯了。

一个shunt有点像mock,一个没有重写全部方法的mock。用这种方法,你能够mock一些方法,而后测试其余的方法。一个单元测试能够由几个shunts来完成,它们重写了相同的类,每一个shunt测试了类的不一样部分。Shunt一般状况下为嵌套类。

终场表演的时候到了!看一下单元测试代码!

 1   public class TestFarmServlet extends TestCase
 2 
 3     {
 4 
 5         static class FarmServletShunt extends FarmServlet
 6 
 7         {
 8 
 9             FarmEJBRemote getRemote_return = null;
10 
11             FarmEJBRemote getRemote()
12 
13             {
14 
15                 return getRemote_return;
16 
17             }
18 
19         }
20 
21         public void testAddAnimal() throws Exception
22 
23         {
24 
25             MockRemote mockRemote = new MockRemote();
26 
27             FarmServletShunt shunt = new FarmServletShunt();
28 
29             shunt.getRemote_return = mockRemote();
30 
31  
32 
33             // just another mock to make
34 
35             MockServletData mockServletData = new MockServletData(); 
36 
37             mockServletData.getParameter_returns.put("species","dog");
38 
39             mockServletData.getParameter_returns.put("buildingID","27");
40 
41  
42 
43             shunt.doAction( mockServletData );
44 
45             assertEquals( 1 , mockRemote.addAnimal_calls );
46 
47             assertEquals( "dog" , mockRemote.addAnimal_species );
48 
49             assertEquals( 27 , mockRemote.addAnimal_buildingID );
50 
51         }
52 
53     }

 

基本的测试框架咱们就展现完了。下面我要和你们分享一个和单元测试有关的概念——依赖注入,也是咱们的单元测试中要到的,敬请期待。

相关文章
相关标签/搜索