依賴注入和單元測試


上一篇博文介紹了測試的相關概念,這篇主要說一下依賴注入以及如何在單元測試中使用。原文鏈接:

http://www.javaranch.com/journal/200709/dependency-injection-unit-testing.html 

近些年來對於依賴注入(Dependency Injection)這個詞大家已經應該很熟悉了。我們經常使用它因為這是一個非常好的面向對象概念。你可能也聽說過Spring框架(Spring Framework),就是所謂的依賴注入容器,在你的印象里面依賴注入和Spring是等同的。但這個想法是錯誤的,依賴注入是一個很簡單的概念,它可以被應用到任何地方,除了依賴注入容器之外,它同樣能夠被應用到單元測試中。這篇文章我們討論一下幾點:

  • 什么是依賴注入
  • 如何實現一個友好的依賴注入類
  • 為什么依賴注入可以使單元測試更加簡單

Ladies and gentlemen,開動你的引擎!

1. 一輛簡單的car

首先我們考慮一個簡單的例子,這里我們使用engine 類和car 類。為了更加清楚的描述問題,我們將類和接口都置空。每輛car會有一個engine,我們想給car裝備上著名的MooseEngine。

Engine類如下:

 1 public interface Engine {  2  3 }  4  5 public class SlowEngine implements Engine {  6  7 }  8  9 public class FastEngine implements Engine { 10 11 } 12 13 public class MooseEngine implements Engine { 14 15 }

 

然后我們可以得到一個car類:

1 public class Car { 2 3 private MooseEngine engine; 4 5 }

這是一輛非常棒的汽車,但是即使有其他種類的引擎上市,我們也不能裝備這些引擎了。我們說這里的car類和MooseEngine類是緊耦合的(tightly coupled)。雖然MooseEngine很棒,但是如果我們想把它換成別的引擎呢?

 

2. 接口編程

 你可能已經注意到了MooseEngine實現了Engine接口。其它引擎也實現了同樣的接口。我們可以想一想,當我們設計我們的Car類時,我們想讓一輛“car”裝備一個“engine”。所以我們重新實現一個Car類,這次我們使用Engine接口:

1 public class Car { 2 3 private Engine engine; 4 5 }

接口編程是依賴注入中的一個很重要的概念。我聽到了你的尖叫,“等一下,你在這里使用接口,具現類(concrete class)該怎么辦?你在哪里設置(set)引擎?我想在我的汽車中裝備MooseEngine”。我們可以按下面的方式來設置它:

1 public class Car { 2 3 private Engine engine = new MooseEngine(); 4 5 }

但這就是有用的么?它看上去和第一個例子沒有多大區別。我們的car仍然同MooseEngine是緊耦合的。那么,我們該如何設置(set或者說注入(inject))我們的汽車引擎呢?

3. 依賴注入介紹

就像依賴注入這個名字一樣,依賴注入就是注入依賴,或者簡單的說,設置不同實例之間的關系。一些人將它同好萊塢的一條規矩關聯了起來,“不要給我打掉話,我打給你。”我更喜歡叫它“bugger”法則:“我不關心你是誰,按我說的做。”在我們的第一個例子中,Car依賴的是Engine的具現類MooseEngine。當一個類A依賴於另外一個類B的時候,類B的實現直接在類A中設置,我們說A緊耦合於B。第二個例子中,我們決定使用接口來代替 具現類MooseEngine,這樣就使得Car類更加靈活。並且我們決定不去定義engine的具現類實現。換句話說,我們使Car類變為松耦合(loosely coupled)的了。Car不再依賴於任何引擎的具現類了。那么在哪里指定我們需要使用哪個引擎呢?依賴注入該登場了。我們不在Car類中設置具現化的Engine類,而是從外面注入。這又該如何實現呢? 

3.1 使用構造函數來注入依賴

設置依賴的一種方法是把依賴類的具體實現傳遞給構造函數。Car類將會變成下面這個樣子:

 1 public class Car {  2  3 private Engine engine;  4  5 public Car(Engine engine) {  6  7 this.engine = engine;  8  9  } 10 11 }

然后我們就可以用任何種類的engine來創建Car了。例如,一個car使用MooseEngine,另外一個使用crappy SlowEngine:

 1 public class Test {  2  3 public static void main(String[] args) {  4  5 Car myGreatCar = new Car(new MooseEngine());  6  7 Car hisCrappyCar = new Car(new SlowEngine());  8  9  } 10 11 }

3.2 使用setter來注入依賴

另外一種設置依賴的普通方法就使用setter方法。當需要注入很多依賴的時候,建議使用setter方法而不是構造函數。我們的car類將會被實現成下面的樣子:

 1 public class Car {  2  3 private Engine engine;  4  5 public void setEngine(Engine engine) {  6  7 this.engine = engine;  8  9  } 10 11 }

它和基於構造函數的依賴注入非常類似,於是我們可以用下面的方法來實現上面同樣的cars:

 1 public class Test {  2  3 public static void main(String[] args) {  4  5 Car myGreatCar = new Car();  6  7 myGreatCar.setEngine(new MooseEngine());  8  9 Car hisCrappyCar = new Car(); 10 11 hisCrappyCar.setEngine(new SlowEngine()); 12 13  } 14 15 }

 

4. 在單元測試中使用依賴注入

如果你將Car類的第一個例子同使用setter依賴注入的例子進行比較,你可能認為后者使用了額外的步驟來實現Car類的依賴注入。這沒錯,你必須實現一個setter方法。但是當你在做單元測試的時候,你會感覺到這些額外的工作都是值得的。如果你對單元測試不熟悉,推薦你看一下這個帖子單元測試有毒 。我們的Car的例子太簡單了,並沒有把依賴注入對單元測試的重要性體現的很好。因此我們不再使用這個例子,我們使用前面已經講述過的關於篝火故事的例子,特別是在在單元測試中使用mock中的部分。我們有一個servlet類,通過使用遠端EJB來在農場中”注冊”動物:

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

你已經注意到了FarmServlet被緊耦合到了FarmEJBRemote實例中,通過調用“FarmEJBUtil.getHome().create()”來取回實例值。這么做會非常難做單元測試。當作單元測試的時候,我們不想使用任何數據庫。我們也不想訪問EJB服務器。因為這不僅會使單元測試很難進行而且會使其變慢。所以為了能夠順利的為FarmServlet類做單元測試,最好使其變成松耦合的。為了清除FarmServlet和FarmEJBRemote之間的緊依賴關系,我們可以使用基於setter的依賴注入:

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

在真實的部署包中,我們確保通過調用“FarmEJBUtil.getHome().create()”而創建的一個FarmServlet遠端成員實例會被注入。在我們的單元測試中,我們使用一個虛擬的mock類來模擬FarmEJBRemote。換句話說,我們通過使用mock類來實現FarmEJBRemote:

 1 class MockFarmEJBRemote implements FarmEJBRemote {  2  3 private String species = null;  4  5 private String buildingID = null;  6  7 private int nbCalls = 0;  8  9 public void addAnimal( String species , String buildingID ) 10 11  { 12 13 this.species = species ; 14 15 this.buildingID = buildingID ; 16 17 this.nbCalls++; 18 19  } 20 21 public String getSpecies() { 22 23 return species; 24 25  } 26 27 public String getBuildingID() { 28 29 return buildingID; 30 31  } 32 33 public int getNbCalls() { 34 35 return nbCalls; 36 37  } 38 39 } 40 41 42 43 public class TestFarmServlet extends TestCase { 44 45 public void testAddAnimal() throws Exception { 46 47 // Our mock acting like a FarmEJBRemote 48 49 MockFarmEJBRemote mockRemote = new MockFarmEJBRemote(); 50 51 // Our servlet. We set our mock to its remote dependency 52 53 FarmServlet servlet = new FarmServlet(); 54 55  servlet.setRemote(mockRemote); 56 57 58 59 // just another mock acting like a ServletData 60 61 MockServletData mockServletData = new MockServletData(); 62 63 mockServletData.getParameter_returns.put("species","dog"); 64 65 mockServletData.getParameter_returns.put("buildingID","27"); 66 67 68 69  servlet.doAction( mockServletData ); 70 71 assertEquals( 1 , mockRemote.getNbCalls() ); 72 73 assertEquals( "dog" , mockRemote.getSpecies() ); 74 75 assertEquals( 27 , mockRemote.getBuildingID() ); 76 77  } 78 79 }

這樣很容易就能測試FarmServlet了。

5. 總結

  • 使用接口而非具現類來表現依賴。
  • 避免在類中隱式的設置(set)一個依賴的具體實現
  • 依賴的具體實現有很多種方法,包括基於構造函數的依賴注入和基於setter的依賴注入
  • 依賴注入使單元測試變的非常靈活。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM