技术开发 频道

应用设计模式编写易于单元测试的代码

  通过 Factory Method 替换被创建对象可以满足一些修改程序运行路径的需求,但是,这种方法以子类化为前提,具有很强的侵入性,并且在编写单元测试时,开发人员需要同时负责 Mock Objects 的开发,供 Factory Method 调用,因此,编码量往往会比较大,单元测试开发人员也需对所使用的公共模块的内部结构有十分清楚的认识。即使可以使用公共的 Mock Objects 实现避免代码重复,往往也需要修改业务逻辑中公共服务相关对象的创建代码,这一点对于应用公共模块的业务逻辑的单元测试可能不太适合。

  在笔者曾参与设计、开发的某应用系统中,有一个专门的数据库缓冲(Cache)公共服务,该 Cache 负责完成与数据库交互,实现数据的存取,并缓存数据以提高后续访问的效率。对于涉及数据库缓冲的业务逻辑的单元测试,需要一个替代方案来替代已有的数据库缓冲,以避免直接访问实际数据库,但又要保证这个替换不会影响到被测试单元的实现。

  为了解决这个问题,我们并没有直接替换 Cache 创建处的代码,因为这些代码遍布在业务代码中,直接替换 Cache 创建代码无疑会侵入业务逻辑,并需要大量使用子类化。为了尽可能降低对业务逻辑的影响,我们维持了原有 CacheFactory 的接口,但是将 CacheFactory 的实现委托(Delegate)给另一个实现类完成,以下是 CacheFactory 实现的伪代码:

1 package com.cachefactory.demo;
2 public abstract class CacheFactory {
3     private static CacheFactory instance = new DelegateCacheFactory();
4     private static CacheFactory delegate;
5   
6     // DelegateCacheFactory is a singleton
7     public static CacheFactory getInstance() {
8         return instance;
9     }
10     
11     // the implementation can be changed
12     protected static void setDelegate(CacheFactory instance) {
13         delegate= instance;
14     }
15     
16     public abstract Cache getCache(Object... args);
17
18     // redirect all request to delegatee
19     private static class DelegateCacheFactory extends CacheFactory {
20         public Cache getCache(Object... args) {
21             return delegate.getCache(args);
22         }
23     }
24 }

  与 CacheFactoryImpl 类似地,我们实现了一个 MockCacheFactory,以处理 getCache 请求并创建 MockCache。

  单元测试时,只需要在 setUp 中执行如下操作:

1 setDelegate(new MockCacheFactory());

  将 CacheFactory 的实现委托给 MockCacheFactory 即可,所有业务逻辑都无需作任何修改,因此,这种替换实现的方式几乎是没有侵入性的。

  这种通过将实现分离到专门的实现类中的做法其实是 Bridge 模式的一个应用,通过使用 Bridge 模式,为替换实现保留了接口,从而使得在不对业务逻辑作任何修改的情况下可以轻松替换公共服务的实现。

  除此之外,Strategy 模式也是一种替换实现的有效途径,这种方式与 Factory Method 类似,通过子类化实现新的 Strategy 以替换业务逻辑使用的旧的 Strategy,通过与 Factory Method 或 Bridge 等模式联合使用,在编写应用公共服务的业务逻辑的单元测试时也十分有用。

  绕过部分实现

  绕过部分实现进行单元测试在大多数情况下是不可取的,因为这种做法极有可能会影响单元测试的质量。但是对于一些特殊的情况,我们可以“冒险”使用这种方式,比如有这样的一个场景:所有请求需经过多级认证,且部分认证处理需要访问数据库,认证结束后为请求分配相应的 sessionId,请求在获得 sessionId 后继续进行进一步的业务逻辑处理。

  在保证多级认证模块已被专门的单元测试覆盖的情况下,我们在为业务逻辑编写单元测试的过程中可以考虑跳过多级认证授权模块(对于部分特权用户,也应跳过部分检查),直接为其分配一个 Mock 的 sessionId,以进行后续处理。

  对于多级认证问题本身,我们可以考虑采用 Chain of Responsibility 模式将不同的认证逻辑封装到不同的 RequestHandler 中,并通过编码或者根据配置,将所有的 Handler 串联成 Responsibility Chain ;而在单元测试过程中,可以修改 Handler 的串联方式,绕过部分不希望在单元测试中经过的 Handler,从而简化单元测试的运行。

  对于这个问题,笔者并不同意为了单元测试的需要去采用 Chain of Responsibility 模式,实际上,上面所阐述的多级认证问题本身比较适合采用这种模式来解决,能够根据需要绕过部分实现,只是应用这种模式的情况下进行单元测试的一种可以考虑的测试途径。

  总结

  单元测试是软件开发的重要组成部分,而应用 Mock Object 是进行单元测试一种普遍而有效的方式,通过在软件设计、开发的过程中合理地运用设计模式,不但为系统重构、功能扩展及代码维护提供了方便,同时也为单元测试的实施提供了极大的灵活性,可以有效降低单元测试编码的难度,方便地在单元测试中引入 Mock Objects,达到对被测试目标进行单元测试的目的,从而更好地保证软件开发的质量。

0
相关文章