上一篇博文介绍了测试的相关概念,这篇主要说一下依赖注入以及如何在单元测试中使用。原文链接:
http://www.javaranch.com/journal/200709/dependency-injection-unit-testing.html
近些年来对于依赖注入(Dependency Injection)这个词大家已经应该很熟悉了。我们经常使用它因为这是一个非常好的面向对象概念。你可能也听说过 Spring 框架(Spring Framework), 就是所谓的依赖注入容器,在你的印象里面依赖注入和 Spring 是等同的。但这个想法是错误的,依赖注入是一个很简单的概念,它可以被应用到任何地方,除了依赖注入容器之外,它同样能够被应用到单元测试中。这篇文章我们讨论一下几点:
Ladies and gentlemen, 开动你的引擎!
首先我们考虑一个简单的例子,这里我们使用 engine 类和 car 类。为了更加清楚的描述问题,我们将类和接口都置空。每辆 car 会有一个 engine,我们想给 car 装备上著名的 MooseEngine。
Engine 类如下:
- 1publicinterfaceEngine {23}45publicclass SlowEngineimplementsEngine {67}89publicclass FastEngineimplementsEngine {1011}1213publicclass MooseEngineimplementsEngine {1415 }
然后我们可以得到一个 car 类:
- 1publicclassCar {23privateMooseEngine engine;45 }
这是一辆非常棒的汽车,但是即使有其他种类的引擎上市,我们也不能装备这些引擎了。我们说这里的 car 类和 MooseEngine 类是紧耦合的(tightly coupled)。虽然 MooseEngine 很棒,但是如果我们想把它换成别的引擎呢?
你可能已经注意到了 MooseEngine 实现了 Engine 接口。其它引擎也实现了同样的接口。我们可以想一想,当我们设计我们的 Car 类时,我们想让一辆 "car" 装备一个 "engine"。所以我们重新实现一个 Car 类,这次我们使用 Engine 接口:
- 1publicclassCar {23privateEngine engine;45 }
接口编程是依赖注入中的一个很重要的概念。我听到了你的尖叫,"等一下,你在这里使用接口,具现类(concrete class)该怎么办?你在哪里设置(set)引擎?我想在我的汽车中装备 MooseEngine"。我们可以按下面的方式来设置它:
- 1publicclassCar {23private Engine engine =newMooseEngine();45 }
但这就是有用的么?它看上去和第一个例子没有多大区别。我们的 car 仍然同 MooseEngine 是紧耦合的。那么,我们该如何设置 (set 或者说注入(inject)) 我们的汽车引擎呢?
就像依赖注入这个名字一样,依赖注入就是注入依赖,或者简单的说,设置不同实例之间的关系。一些人将它同好莱坞的一条规矩关联了起来,"不要给我打掉话,我打给你。" 我更喜欢叫它 "bugger" 法则:"我不关心你是谁,按我说的做。" 在我们的第一个例子中,Car 依赖的是 Engine 的具现类 MooseEngine。当一个类 A 依赖于另外一个类 B 的时候,类B的实现直接在类A中设置,我们说A紧耦合于 B。第二个例子中,我们决定使用接口来代替 具现类 MooseEngine,这样就使得 Car 类更加灵活。并且我们决定不去定义 engine 的具现类实现。换句话说,我们使 Car 类变为松耦合 (loosely coupled) 的了。Car 不再依赖于任何引擎的具现类了。那么在哪里指定我们需要使用哪个引擎呢?依赖注入该登场了。我们不在 Car 类中设置具现化的 Engine 类,而是从外面注入。这又该如何实现呢?
设置依赖的一种方法是把依赖类的具体实现传递给构造函数。Car 类将会变成下面这个样子:
- 1publicclassCar {23privateEngine engine;45publicCar(Engine engine) {67this.engine =engine;89}1011 }
然后我们就可以用任何种类的 engine 来创建 Car 了。例如,一个 car 使用 MooseEngine,另外一个使用 crappy SlowEngine:
- 1publicclassTest {23publicstaticvoidmain(String[] args) {45 Car myGreatCar =new Car(newMooseEngine());67 Car hisCrappyCar =new Car(newSlowEngine());89}1011 }
另外一种设置依赖的普通方法就使用 setter 方法。当需要注入很多依赖的时候,建议使用 setter 方法而不是构造函数。我们的 car 类将会被实现成下面的样子:
- 1publicclassCar {23privateEngine engine;45publicvoidsetEngine(Engine engine) {67this.engine =engine;89}1011 }
它和基于构造函数的依赖注入非常类似,于是我们可以用下面的方法来实现上面同样的 cars:
- 1publicclassTest {23publicstaticvoidmain(String[] args) {45 Car myGreatCar =newCar();67 myGreatCar.setEngine(newMooseEngine());89 Car hisCrappyCar =newCar();1011 hisCrappyCar.setEngine(newSlowEngine());1213}1415 }
如果你将 Car 类的第一个例子同使用 setter 依赖注入的例子进行比较,你可能认为后者使用了额外的步骤来实现 Car 类的依赖注入。这没错,你必须实现一个 setter 方法。但是当你在做单元测试的时候,你会感觉到这些额外的工作都是值得的。如果你对单元测试不熟悉,推荐你看一下这个帖子单元测试有毒 。我们的 Car 的例子太简单了,并没有把依赖注入对单元测试的重要性体现的很好。因此我们不再使用这个例子,我们使用前面已经讲述过的关于篝火故事的例子,特别是在在单元测试中使用 mock 中的部分。我们有一个 servlet 类,通过使用远端 EJB 来在农场中 "注册" 动物:
- 1publicclass FarmServletextendsActionServlet {23publicvoid doAction( ServletData servletData )throwsException {45 String species = servletData.getParameter("species");67 String buildingID = servletData.getParameter("buildingID");89if ( Str.usable( species ) &&Str.usable( buildingID ) ) {1011 FarmEJBRemote remote =FarmEJBUtil.getHome().create();1213remote.addAnimal( species , buildingID );1415}1617}1819 }
你已经注意到了 FarmServlet 被紧耦合到了 FarmEJBRemote 实例中,通过调用 "FarmEJBUtil.getHome().create()" 来取回实例值。这么做会非常难做单元测试。当作单元测试的时候,我们不想使用任何数据库。我们也不想访问 EJB 服务器。因为这不仅会使单元测试很难进行而且会使其变慢。所以为了能够顺利的为 FarmServlet 类做单元测试,最好使其变成松耦合的。为了清除 FarmServlet 和 FarmEJBRemote 之间的紧依赖关系,我们可以使用基于 setter 的依赖注入:
- 1publicclass FarmServletextendsActionServlet {23privateFarmEJBRemote remote;45publicvoidsetRemote(FarmEJBRemote remote) {67this.remote =remote;89}1011publicvoid doAction( ServletData servletData )throwsException {1213 String species = servletData.getParameter("species");1415 String buildingID = servletData.getParameter("buildingID");1617if ( Str.usable( species ) &&Str.usable( buildingID ) ) {1819remote.addAnimal( species , buildingID );2021}2223}2425 }
在真实的部署包中,我们确保通过调用 "FarmEJBUtil.getHome().create()" 而创建的一个 FarmServlet 远端成员实例会被注入。在我们的单元测试中,我们使用一个虚拟的 mock 类来模拟 FarmEJBRemote。换句话说,我们通过使用 mock 类来实现 FarmEJBRemote:
- 1class MockFarmEJBRemoteimplementsFarmEJBRemote {23private String species =null;45private String buildingID =null;67privateint nbCalls = 0;89publicvoidaddAnimal( String species , String buildingID )1011{1213this.species =species ;1415this.buildingID =buildingID ;1617this.nbCalls++;1819}2021publicString getSpecies() {2223returnspecies;2425}2627publicString getBuildingID() {2829returnbuildingID;3031}3233publicintgetNbCalls() {3435returnnbCalls;3637}3839}40414243publicclass TestFarmServletextendsTestCase {4445publicvoid testAddAnimal()throwsException {4647//Our mock acting like a FarmEJBRemote4849 MockFarmEJBRemote mockRemote =newMockFarmEJBRemote();5051//Our servlet. We set our mock to its remote dependency5253 FarmServlet servlet =newFarmServlet();5455servlet.setRemote(mockRemote);56575859//just another mock acting like a ServletData6061 MockServletData mockServletData =newMockServletData();6263 mockServletData.getParameter_returns.put("species","dog");6465 mockServletData.getParameter_returns.put("buildingID","27");66676869servlet.doAction( mockServletData );7071 assertEquals( 1, mockRemote.getNbCalls() );7273 assertEquals( "dog", mockRemote.getSpecies() );7475 assertEquals( 27, mockRemote.getBuildingID() );7677}7879 }
这样很容易就能测试 FarmServlet 了。
来源: http://www.cnblogs.com/harlanc/p/6876462.html