一文幫你理解什麼是單元測試

最近想對咱們的單元測試作一下總結,樓主在平常工做中寫了很多單元測試,但有些概念和用法並無刨根問題的去追尋,研究。因而把一些不清晰的概念輸入到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     }

 

基本的測試框架咱們就展現完了。下面我要和你們分享一個和單元測試有關的概念——依賴注入,也是咱們的單元測試中要到的,敬請期待。

相關文章
相關標籤/搜索