zoukankan      html  css  js  c++  java
  • 依赖注入和单元测试

    上一篇博文介绍了测试的相关概念,这篇主要说一下依赖注入以及如何在单元测试中使用。原文链接:

    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的依赖注入
    • 依赖注入使单元测试变的非常灵活。
  • 相关阅读:
    BEM(Block–Element-Modifier)
    http://element.eleme.io/#/zh-CN/component/quickstart
    Commit message 的写法规范。本文介绍Angular 规范(
    好的commit应该长啥样 https://github.com/torvalds/linux/pull/17#issuecomment-5654674
    代码管理
    if you have content fetched asynchronously on pages where SEO is important, SSR might be necessary
    Martin Fowler’s Active Record design pattern.
    The Zen of Python
    Introspection in Python How to spy on your Python objects Guide to Python introspection
    Object-Oriented Metrics: LCOM 内聚性的度量
  • 原文地址:https://www.cnblogs.com/harlanc/p/6876462.html
Copyright © 2011-2022 走看看