用jMock来实现这一点,然而,我们至少需要定义PendingOrderRepository接口并且写一个PendingOrder的stub(桩)实现。PendingOrderRepository接口定义了一个findOrCreatePendingOrder()方法:
<o:p> </o:p>
public interface PendingOrderRepository{
<o:p> </o:p>
PendingOrder findOrCreatePendingOrder(String pendingOrderId);
}
<o:p> </o:p>
PendingOrder类为updateDeliveryInfo()方法定义了一个桩,该桩将在后面一点实现:
<o:p> </o:p>
public class PendingOrder{
<o:p> </o:p>
public boolean updateDeliveryInfo(Address deliveryAddress,
Date deliveryTime){
return false;
}
}
<o:p> </o:p>
updateDeliveryInfo()方法的桩返回假。
<o:p> </o:p>
完成测试
<o:p> </o:p>
我们已经编写了updateDeliveryInfo()方法并且定义了PendingOrderRepository接口和PendingOrder类,但是这时我们需要修正一下我们之前编写的测试程序。它不需要再编译一次因为PlaceOrderServiceImpl的构造方法通过被传递了一个PendingOrderRepository参数而具有了我们预期的行为。另外,测试程序必须创建和配置模拟的(假的)PendingOrder和PendingOrderRepository。这个模拟的(mock)PendingOrderRepository被期望来调用它的findOrCreatePendingOrder()方法并且返回一个模拟的(mock)PendingOrder。PendingOrder被期望来调用它自己的updateDeliveryInfo()方法。表3.4显示了一个更新测试。
<v:shapetype id="_x0000_t75" path="m@4@5l@4@11@9@11@9@5xe" o:preferrelative="t" filled="f" stroked="f" coordsize="21600,21600" o:spt="75"><v:stroke joinstyle="miter"></v:stroke><v:formulas><v:f eqn="if lineDrawn pixelLineWidth 0"></v:f><v:f eqn="sum @0 1 0"></v:f><v:f eqn="sum 0 0 @1"></v:f><v:f eqn="prod @2 1 2"></v:f><v:f eqn="prod @3 21600 pixelWidth"></v:f><v:f eqn="prod @3 21600 pixelHeight"></v:f><v:f eqn="sum @0 0 1"></v:f><v:f eqn="prod @6 1 2"></v:f><v:f eqn="prod @7 21600 pixelWidth"></v:f><v:f eqn="sum @8 21600 0"></v:f><v:f eqn="prod @7 21600 pixelHeight"></v:f><v:f eqn="sum @10 21600 0"></v:f></v:formulas><v:path o:connecttype="rect" o:extrusionok="f" gradientshapeok="t"></v:path><o:lock aspectratio="t" v:ext="edit"></o:lock></v:shapetype><v:shape id="_x0000_i1027" style="WIDTH: 6in; HEIGHT: 396pt" type="#_x0000_t75"><v:imagedata o:title="3" src="file:///C:\DOCUME~1\azhang\LOCALS~1\Temp\msohtml1\03\clip_image001.jpg"></v:imagedata></v:shape>
<v:shape id="_x0000_i1028" style="WIDTH: 6in; HEIGHT: 243pt" type="#_x0000_t75"><v:imagedata o:title="3" src="file:///C:\DOCUME~1\azhang\LOCALS~1\Temp\msohtml1\03\clip_image003.jpg"></v:imagedata></v:shape>
<v:shape id="_x0000_i1026" style="WIDTH: 6in; HEIGHT: 225pt" type="#_x0000_t75"><v:imagedata o:title="3" src="file:///C:\DOCUME~1\azhang\LOCALS~1\Temp\msohtml1\03\clip_image005.jpg"></v:imagedata></v:shape>
<o:p> </o:p>
让我们来看看这段测试程序做了哪些事
<o:p> </o:p>
1 测试用例继承了jMock的MockObjectTestCase类,它能自动的检验mock对象的预期行为是否能被满足。
<o:p> </o:p>
2 setUp()方法创建了模拟的(mock)PendingOrderRepository、PendingOrder和PlaceOrderService。
<o:p> </o:p>
3 测试程序定义了mock对象的预期行为和返回结构。模拟的PendingOrderRepository被计划用发货信息来调用findOrCreatePendingOrder()并且返回模拟的(mock)PendingOrder。模拟的(mock)PendingOrder被计划通过以发货信息为参数来调用其updateDeliveryInfo()方法。它返回true则表示发货信息是有效的而且PendingOrder被更新了
<o:p> </o:p>
4 在配置了预期行为之后,测试程序调用了PlaceOrderServe的updateDeliveryInfo()方法。
<o:p> </o:p>
5 然后测试程序断言调用是否成功,并且检验PlaceOrderService返回的模拟的(mock)PendingOrder。它不再检验PendingOrder是否包含了正确的发货信息因为它假设PendingOrder.updateDeliveryInfo()的行为是正确的。
<o:p> </o:p>
正如你能看到的,这个方法和它的测试程序有着很简单的关联关系,因为大多数的业务方法只是很简单的调用其他领域模型的对象(或者是领域模型中的其他对象)。因为利用了这些mock对象,我们就可以在不投入到它们(PlaceOrderService用到的类)的实现细节中也能开发和测试PlaceOrderService。
<o:p> </o:p>
<o:p> </o:p>
3.3.2 实现一个领域实体的方法
<o:p> </o:p>
我们已经实现了我们的第一个方法,它通过了测试并且能够正常工作!但是我们仍然还有很多事要做。当实现PlaceOrderService.updateDeliveryInfo()时,我们决定把它的职责分配给PendingOrder,PendingOrder是一个领域模型中的实体(entity),然后调要它的updateDeliveryInfo()方法。这个方法检验发货信息并且更新PendingOrder。它返回一个能表明发货信息是否有效的布尔值。让我们来看看如何来实现这个方法。
<o:p> </o:p>
写一个测试
<o:p> </o:p>
像之前那样,我们先写一个测试程序。表3.5是一个简单的测试,通过传递给该方法一个有效的发货信息来测试该方法是否能通过。它使用了不可见的RestaurantTestData来创建一些测试数据。
<v:shape id="_x0000_i1025" style="WIDTH: 486pt; HEIGHT: 435pt" type="#_x0000_t75"><v:imagedata o:title="3" src="file:///C:\DOCUME~1\azhang\LOCALS~1\Temp\msohtml1\03\clip_image006.jpg"></v:imagedata></v:shape>
<o:p> </o:p>
该测试用例用一个有效的发货信息调用了PendingOrder.updateDeliveryInfo()方法并且检验它是否更新了PendingOrder,返回值是否为真。
<o:p> </o:p>
实现方法
<o:p> </o:p>
因为PendingOrder已经定义了一个桩方法,那这个测试编译的时候就不会有问题。但是为了使之运行通过,我们需要用一个真实的实现来代替这个桩,从而可以使之可以检验发货信息和更新PendingOrder。
<o:p> </o:p>
PendingOrder首先用Calendar类来检查发货信息中的发货时间是否在一小时以后。然后查询数据库来检验发货信息。最简单的方法是在RestaurantRepository里封装这个查询并且定义isRestaurantAvailable()方法。updateDeliveryInfo()方法调用isRestaurantAvailable,如果其返回值为真就把发货信息存储到数据库。
<o:p> </o:p>
<v:shape id="_x0000_i1029" style="WIDTH: 6in; HEIGHT: 410.25pt" type="#_x0000_t75"><v:imagedata o:title="3" src="file:///C:\DOCUME~1\azhang\LOCALS~1\Temp\msohtml1\03\clip_image007.jpg"></v:imagedata></v:shape>
一个我们没有解决的很重要的设计上的问题是,PendingOrder如何访问RestaurantRepository。让我们来看看如何解决这个问题。
<o:p> </o:p>
选择如何访问一个repository
<o:p> </o:p>
Repositories主要是被领域中的service类来使用,但是他们也被一些实体调用,比如说entities。为调用一个repository对象的方法,调用者必须显示的拥有整个对象的引用。之前你看到如何把repository作为一个参数传递给PlaceOrderService的。然而,在领域模型的entity中有时候这却是行不通的,且听我娓娓道来。让我们把问题揭示出来然后再给大家讲讲几种解决方案。
<o:p> </o:p>
最方便的方法是把repository作为参数传递给entities,就像传递给service一样。这样可以使用轻量级容器的构造方法注入机制来初始化实体。把repository作为参数传递给构造方法比传递给一般的方法要简单得多而且也没有使用单例模式时的缺点了,我将在后面一点讲解单例模式。然而使用这种方法来初始化entity不是易懂的,因为它不同于service,service由轻量级容器来负责初始化,而entity则由持久化框架在从数据库里读取数据的后创建。
<o:p> </o:p>
默认情况下,持久化框架是直接使用类的默认构造方法来创建对象的,所以不可能把任何需要的对象传递进它(将被创建的entity)构造方法。一些(不是所有的)持久框架有配置对象初始化的机制,这样可以允许程序来控制如何初始化entity。程序可以用具有依赖注入的轻量级框架来配置持久化框架以便于创建对象。比如说使用hibernate 的构造器注入。然而,因为该方法并不是一种普遍的方法,所以在本书中我不会使用它。
<o:p> </o:p>
另一个选择是用静态方法和变量来实现repository。举个例子,你可以用ThreadLocal模式或者单例模式来实现一个repository。这种方法可以使用在任何的持久化框架中,而且不需要把repository传来传去,但是有时会把代码弄得太复杂。使用静态方法和变量的问题是它们会把代码弄得难以测试。比如说,它们阻止你使用一个可变的实现如一个mock对象,因为你不能改变程序对一个静态方法的调用,也不能改变它来访问另一个类的静态变量。它们也引入了隐藏的依赖关系,因为代码取决于那些必须被初始化的静态变量。因此,最好避免使用静态方法和静态变量。
<o:p> </o:p>
知道了只有一些持久化框架允许你使用构造方法注入来初始化entity并且使用静态方法和变量有很多严重的缺点,那么把repository作为参数传递给方法就有意义了。它避免了使用单例模式的缺点也不设计到所有的持久化框架的特点。然而,采用这种方法的一个缺点是,它在代码中有一个一石激起千层浪的效果(ripple effect)。我们也许不得不改变很多方法来把repository作为一个参数传递给该方法使用,这个repository由service传递过来,service是通过构造器注入来拥有了repository的引用的。
<o:p> </o:p>
在这个例子中,把RestaurantRepository传递给PendingOrder只需要作很小的改动。我们只需要改动PlaceOrderService来把RestaurantRepository作为一个参数传递给PendingOrder的updateDeliveryInfo()方法,而RestaurantRepository应该是PlaceOrderServie的构造方法的一个参数。
表3.6显示了PendingOrder.updateDeliveryInfo()方法。
<v:shape id="_x0000_i1030" style="WIDTH: 450pt; HEIGHT: 372pt" type="#_x0000_t75"><v:imagedata o:title="3" src="file:///C:\DOCUME~1\azhang\LOCALS~1\Temp\msohtml1\03\clip_image009.jpg"></v:imagedata></v:shape>
<o:p> </o:p>
这个方法调用了RestaurantRepository.isRestaurantAvailable()方法,如果返回true,那么就把发货信息更新到PendingOrder。
<o:p> </o:p>
为了使这个类通过编译,我们需要定义isRestaurantAvailable()方法;
<o:p> </o:p>
public interface RestaurantRepository{
boolean isRestaurantAvailable (Address deliveryAddress,
Date deliveryTime)
}
<o:p> </o:p>
如果至少有一家餐馆提供了发货信息中指定的服务则返回true。我们也不得不改变PlaceOrderService来把RestaurantRepository传递给其构造方法,并且也要把RestaurantRepository传递给PendingOrder ,同时要用模拟的(mock)RestaurantRepository来改变PendingOrder的测试类。