我们将从构建简单的驼鹿监视软件开始示例各种场景下的测试驱动开发 鹿是鹿家族中的最大个的成员目前世界上估计有百万头为了更好的统计世界驼鹿组织让我们开发一个驼鹿监视软件希望保护人员能够跟蹤和记录他们见到的驼鹿 本文假设读者已经熟悉java/ant/maven/classpath而且至少写过一两个简单的junit测试 Vanilla JUnit 首先编写Moose类 package moose; import javautilDate; public class Moose { private Date dateTimeObserved; private int age; public Moose(Date dateTimeObservedParam int estimatedAge) { thisdateTimeObserved = dateTimeObservedParam; thisage = estimatedAge; } } 虽然驼鹿最长可以生存年但大部分驼鹿在年青的时候就被捕杀通常只活了年根据世界驼鹿组织报告我们想知道某一年龄后的驼鹿数据因此我们增加了isOlderThan(int)方法 单元测试如下 public void testIsOlderThan() { Moose moose = new Moose(new Date() MOOSE_AGE); assertTrue(Moose age + MOOSE_AGE + should of been older than + TEST_AGE mooseisOlderThan(TEST_AGE) ); } 方法: boolean isOlderThan(int contenderAge) { return thisage >= contenderAge; } 这是一个普通的junit测试用例这种方式通常用在测试驱动的开发中这种完全独立的方法一般是很少的通常我们都需要一些代价昂贵的或比较难构建的外部资源 使用伪对象和jMock 观察驼鹿的最佳时间是在黎明和黄昏跟其他动物的观察时间相似WOM希望我们提供获取记录驼鹿被发现的时间的保护人员这样就可以通过String getObserverName()来获取相应的驼鹿了 不幸地是保护人员的接口来自第三方接口PersonnelUnit而这是个大家伙没有LDAP我们是无法构造的 为了测试getObserverName()我们可以启动LDAP服务器发送数据运行测试用例然后再关闭LDAP但那将是集成测试而不是单元测试了对我们来说宁可多写些代码因此我们创建了一个包含我们需要方法的Ranger接口 public interface Ranger { String getName(); } 这给我们第一个启发式的单元测试用接口分离外部依赖 我们需要改变Moose的构造函数增加Ranger参数于是变成 above as before private Ranger observer; public Moose( Date dateTimeObservedParam int estimatedAge Ranger observedBy) { thisdateTimeObserved = dateTimeObservedParam; thisage = estimatedAge; thisobserver = observedBy; } below as before 后面我们会实现Ranger接口作为包含在最终产品中的PersonnelUnit代理但现在我们需要的是一个简单实现返回我们所需要名字的实现—记住我们是在测试Moose而不是Ranger 这种我们通过硬编码或定义方法调用异常的简单实现方式被称为伪对象伪对象是测试中经常需要的东西目前有很多可用的类库供你选择其中最好用的类库之一是jMock他使用JSE中的动态代理使我们在运行时创建接口实现 为了使用jMock我们必须首先修改我们的测试类使用权其继承MockObjectTestCase并且在setup()和teardown()中调用父类中的相应方法 public class TestMoose extends MockObjectTestCase { public void setUp() throws Exception { supersetUp(); } public void tearDown() throws Exception { supertearDown(); } the rest as before 使用jMock后getObserverName()的测试就更简单了 public void testObserverName() { Mock rangerMock = mock(Rangerclass); rangerMockexpects( once() thod(getName)will( returnValue(RANGER_NAME) ); Moose moose = new Moose(new Date() MOOSE_AGE (Ranger) rangerMockproxy() ); assertEquals(Moose did not report correct ranger RANGER_NAME moosegetObserverName() ); } 让我们逐行地解释一下 ·Mock rangerMock = mock(Rangerclass); 这行创建了一个Mock对象来伪装Ranger接口的实现调用rangerMockproxy()返回Ranger ·rangerMockexpects( once() thod(getName)will( returnValue(RANGER_NAME) ); 这里是最有趣的地方他的用途很明显告诉Mock期望getName()方法仅被调用一次并且在被调用后应该返回RANGER_NAME的值在测试的最后当我们的tearDown()方法调用supertearDown()时我们的父类MockObjectTestCase会检查是否所有期望值都已经满足否则就会失败 ·Moose moose = new Moose(new Date() MOOSE_AGE (Ranger) rangerMockproxy() ); 这行创建Moose对象注意我们是如何获取Ranger的实现的通过调用的Mockproxy()方法 ·assertEquals(Moose did not report correct ranger RANGER_NAME moosegetObserverName() ); 最后是测试本身很容易吧因为Ranger接口使用起来很方便我们并不需要经常这样 注册模式 驼鹿喜欢居住吃水池草但他们也是别人的猎物因为驼鹿与别的物种的关系WOM要求我们在发现一只驼鹿的时候给其他的团队发送消息这些消息将被熊/狼/鹿/水池草的保护人员获取 在项目中的消息系统是一个企业级的服务系统如Tibco Rendezvous IBM MQSeries 或JMS的实现就像PersonnelUnit依赖LDAP服务器这种消息系统的要求也使得测试变的困难如何才能使这些对象在没有后台的服务系统时存在呢?我们又如何能得到一个这样的消息系统呢?开始的想法是在集成测试中处理但实际上我们需要的是一个接口和相应的模式 让我们用一个友好的接口来隐藏消息系统吧 public interface Messenger { void sendMessage(String topic Object[] values); } 现在是我们第二个启发式的单元测试用接口标示服务或角色因此他们的名字通常心or/er为结尾 现在我们需要的是如何获取一个Messenger的实现这相对于寻找驼鹿来说简单多了如果你使用IOC容器(如Spring或HiveMind)你已经知道容器会帮你处理Messenger实现否则使用注册模式(服务定位模式的一种)这是一种简单的全局静态图来映射服务名与其实现 public class Registry { private static Map registry = new HashMap(); public static void put(String key Object implementation) { registryput(key implementation); } public static Object get(String key) { return registryget(key); } } 在代码中我们这样使用注册 Messenger messenger = (Messenger) Registryget(MESSENGER); messengersendMessage(A_TOPIC someValues); 跟着是测试用例 public void testMessageIsSent() { Date observationDate = new Date(); Object[] valueArray = new Object[] { observationDate new Integer(MOOSE_AGE) }; Mock messenger = mock(Messengerclass); messengerexpects( once() thod(sendMessage)with( eq(MESSAGE_TOPIC) eq(valueArray) ); Registryput( MESSENGER messengerproxy() ); Moose moose = new Moose(observationDate MOOSE_AGE null ); } 让我们来看一下重要的几行 · Mock messenger = mock(Messengerclass); 创建一个实现Messenger接口的伪对象在产品代码中我们会创建一个与MS Rendezvous或类似需求通讯的实现 ·messengerexpects( once() thod(sendMessage)with( eq(MESSAGE_TOPIC) eq(valueArray) ); 告诉伪对象期望sendMessage()方法只被调用一次包含两个值消息主题和一组消息内容 ·Registryput( MESSENGER messengerproxy() ); 注册接口的伪实现在这里所有获取来自注册表中Messenger的代码会得到我们刚才创建的伪对象 ·Moose moose = new Moose(observationDate MOOSE_AGE null); 最后是我们的测试我知道这看起来不像是个测试但在他运行后会运行tearDown()和调用supertearDown()在那个方法中会检查是否所有期望被满足否则测试失败因此如果sendMessage()方法没有调用我们的伪消息器消息就会失败 数据库测试 我知道你可能会想与LDAP和消息通讯的确不错但在实际中我们通常需要与数据库通讯是的就像我们第一个启发式测试中(通过接口分离外部依赖)让我们简单地用接口来隐藏数据库 public interface StorageManager { void save(Object objectToSave) throws StorageException; } 将实现放在注册表中而代码只需要这样写 StorageManager storageManager = (StorageManager) Registryget( RegistrySTORAGE ); storageManagersave( myObject ); 在这里我们会用StorageException来封装所有实现中的异常 不管用HibernatJDO或者其他持久层实现都很容易将你与JDBC分离开来我知道为每一个创建的对象编写select/insert/update/delete是很无聊的因此我们可以只写一个HibernateStorageManager来实现StorageManager并处理其他细节(如果你必须手写JDBCMockrunner项目可能对你写单元测试有所帮助) 在我们的单元测试中会创建一个StorageManager伪对象并期望save方法被正确的对象调用下面的save()的测试方法 public void testSave()throws StorageException { // Create a mock Messenger than ignores any messages it gets Mock messenger = mock(Messengerclass); messengerstubs(thod(sendMessage); Registryput( RegistryMESSENGER messengerproxy() ); // Create the moose Moose moose = new Moose(new Date() MOOSE_AGE null); // Create a mock StorageManager and tell it what will happen when we save the moose Mock storage = mock(StorageManagerclass); storageexpects( once() thod(save)with( same(moose) ); Registryput(RegistrySTORAGE storageproxy()); // Test ! moosesave(); } 让我们来看一下重要部分首先我们建立一个Messenger桩 messengerstubs(thod(sendMessage); Jmock拥有一次或多次激活的桩如果桩没有被调用或者被多次调用测试也不会失败从先前的部分我们知道如何创建一个Moose并通过Messenger接口来发送消息这个行为与save()方法无关我们不希望他影响我们的测试因此创建一个桩 下一步我们创建测试的Moose对象并在伪对象StorageManager上设置我们的期望值 storageexpects( once() thod(save)with( same(moose) ); 这个看起来很直观我们期望save()方法被调用一次而moose对象被作为参数在内部same()方法用==来比较对象而前面使用的eq()方法会使用equals 最后我们保存的Moose: moosesave(); 一旦测试完成Junit会运行tearDown()方法来检查所有的期望值是否被满足否则测试失败这个测试确保我们在请求Moose保存自己的时候他会将工作代理给StorageManager 当我们实际的StorageManager实现时(如HibernateStorageManager)我们会编写集成测试来确保他正确工作如果两到三个集成测试用例保证HibernateStorageManager正确工作那么你所需要在你单元测试中检查的只胡对象需要正确地将保存工作代理给StorageManager我们测试的保存并不会真正地保存 小结 所有上面的测试用例可以从资源中下载可阅读旁注构建样例程序来运行样例 下面是我在本文中所关注的两个重点 使用接口将你的代码与外部资源分离就像母牛保护她的孩子 使用jMock创建这些接口的伪实现 这两个技巧会的用处就像单元测试的用处就像驼鹿不能出汗因为他们庞大的身体而且热量会通过内髒的发酵过程发散出去我们在测试中也不需要出汗 |