【IT168 技术文章】
引言
设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述,通过在系统设计中引入合适的设计模式可以为系统实现提供更大的灵活性,从而有效地控制变化,更好地应对需求变更或者按需变更系统运行路径等问题。
单元测试是软件开发的一个重要组成部分,是与编码实现同步进行的开发活动,这一点已成为软件开发者的共识。适度的单元测试不但不会影响开发进度,反而可以为开发过程提供很好的控制,为软件质量、系统重构等提供有力的保障,并且,当后续系统需求发生变更、Bug Fix 或功能扩展时,能很好地保证已有实现不会遭到破坏,从而使得程序更易于维护和修改。 Martin Fowler、Kent Beck、Robert Martin 等软件设计领域泰斗更是极力倡导测试先行的测试驱动开发(Test Driven Development,TDD)的开发方式。
单元测试主要用于测试细粒度的程序单元,如类的某个复杂方法的正确性,也可以根据需要综合测试某个操作所涉及的多个相互联系的类的正确性。在很多情况下,相互联系的多个类中有些类比较简单,为这些简单类单独编写单元测试用例往往不如将它们与使用它们的类一起进行测试有意义。
模拟对象(Mock Objects)是为模拟被测试单元所使用的外围对象、设备(后文统一简称为外部对象)而设计的一种特殊对象,它们具有与外部对象相同的接口,但实现往往比较简单,可以根据测试的场景进行定制。由于单元测试不是系统测试,方便、快速地被执行是单元测试的一个基本要求,直接使用外部对象往往需要经过复杂的系统配置,并且容易出现与欲测试功能无关的问题;对于一些异常的场景,直接使用外部对象可能难以构造,而通过设计合适的 Mock Objects,则可以方便地模拟需要的场景,从而为单元测试的顺利执行提供有效的支持。
本文根据笔者经验,介绍了几种典型的设计模式在系统设计中的应用,及由此为编写单元测试带来的方便。
从对象创建开始
由于需要使用 Mock Objects 来模拟外部对象的功能,因此必须修改正常的程序流程,使得被测试功能模块与 Mock Objects,而不是外部对象进行交互。要做到这一点,首先要解决的问题就是对象创建,即在原本创建外部对象的地方创建 Mock Objects,因此在设计、实现业务逻辑时需要注意从业务逻辑中分离出对象创建逻辑。
Factory Method 是一种被普遍运用的创建型模式,用于将对象创建的职责分离到独立的方法中,并通过子类化来实现创建不同对象的目的。如果被测试单元所使用的外部对象是通过 Factory Method 创建的,则可以通过从已有被测试的 Factory 类派生出一个新的 MockFactory,以创建 Mock Objects,并在 setUp 测试中创建 MockFactory,从而间接达到对被测试类进行测试的目的。
下面的代码片段展示了具体的做法:
2 package com.factorymethod.demo;
3 public interface BaseObjects {
4 voidfunc();
5 }
6
7 // OuterObjects.java
8 package com.factorymethod.demo;
9 public class OuterObjects implements BaseObjects {
10 public void func() {
11 System.out.println("OuterObjects.func");
12 }
13 }
14
15 // LogicToBeTested.java, code to be tested
16 package com.factorymethod.demo;
17 public class LogicToBeTested {
18 public void doSomething() {
19 BaseObjects b = createBase();
20 b.func();
21 }
22
23 public BaseObjects createBase() {
24 return newOuterObjects();
25 }
26 }
以下则是对应的 MockOuterObjects、MockFactory 以及单元测试的实现:
2 package com.factorymethod.demo;
3 public class MockOuterObjects implements BaseObjects {
4 public void func() {
5 System.out.println("MockOuterObjects.func");
6 }
7 }
8
9 // MockLogicToBeTested.java
10 package com.factorymethod.demo;
11 public class MockLogicToBeTested extends LogicToBeTested {
12 public BaseObjects createBase() {
13 return new MockOutterObjects();
14 }
15 }
16
17 // LogicTest.java
18 package com.factorymethod.demo;
19 import junit.framework.TestCase;
20
21 public class LogicTest extends TestCase {
22 LogicToBeTested c;
23 protected void setUp() {
24 c =new MockLogicToBeTested();
25 }
26 public void testDoSomething() {
27 c.doSomething();
28 }
29 }
30
Abstract Factory 是另一种被普遍运用的创建型模式,Abstract Factory 通过专门的 Factory Class 来封装对象创建的职责,并通过实现 Abstract Factory 来完成不同的创建逻辑。如果被测试单元所使用的外部对象是通过 Abstract Factory 创建的,则实现一个新的 Concrete Factory,并在此 Factory 中创建 Mock Objects 是一个比较好的解决办法。对于 Factory 本身,则可以在 setUp 测试的时候指定新的 Concrete Factory ;此外,借助依赖注入框架(如 Spring 的 BeanFactory),通过依赖注入的方式将 Factory 注入也是一种不错的解决方案。对于简单的依赖注入需求,可以考虑实现一个应用专有的依赖注入模块,或者实现一个简单的实现加载器,即根据配置文件载入相应的实现,从而无需修改应用代码,仅通过修改配置文件即可载入不同的实现,进而方便地修改程序的运行路径,执行单元测试。
下面的代码实现了一个简单的 InstanceFactory:
2 // pkcs15/src/main/java/org/opensc/pkcs15/asn1/InstanceFactory.java
3 packagecom.instancefactory.demo;
4
5 importjava.lang.reflect.InvocationTargetException;
6 importjava.lang.reflect.Method;
7 importjava.lang.reflect.Modifier;
8
9 public class InstanceFactory {
10 private final Method getInstanceMethod;
11
12 public InstanceFactory(String type) {
13 Class clazz =null;
14 try {
15 clazz = Class.forName(type);
16 this.getInstanceMethod = clazz.getMethod("getInstance");
17 if(!Modifier.isStatic(this.getInstanceMethod.getModifiers())
18 || !Modifier.isPublic(this.getInstanceMethod.getModifiers()))
19 throw new IllegalArgumentException(
20 "Method [" + clazz.getName()
21 + ".getInstance(Object)] is not static public.");
22 } catch (NoSuchMethodException e) {
23 throw new IllegalArgumentException(
24 "Class [" + clazz.getName()
25 + "] has no static getInstance(Object) method.", e);
26 } catch (ClassNotFoundException e) {
27 throw new IllegalArgumentException("Class [" + type + "] is not found");
28 }
29 }
30
31 public Object getInstance() {
32 try{
33 return this.getInstanceMethod.invoke(null);
34 } catch (InvocationTargetException e) {
35 if( e.getCause() instanceof RuntimeException )
36 throw (RuntimeException)e.getCause();
37 throw new IllegalArgumentException(
38 "Method [" +this.getInstanceMethod
39 + "] has thrown an checked exception.", e);
40 } catch( IllegalAccessException e) {
41 throw new IllegalArgumentException(
42 "Illegal access to method ["
43 +this.getInstanceMethod + "].", e);
44 }
45 }
46
47 public Method getGetInstanceMethod() {
48 return this.getInstanceMethod;
49 }
50 }
以下代码演示了 InstanceFactory 的简单使用:
2 package com.instancefactory.demo;
3
4 public interface BaseObjects {
5 voidfunc();
6 }
7
8 // OuterObjects.java
9
10 package com.instancefactory.demo;
11
12 public class OuterObjects implements BaseObjects {
13 public static BaseObjects getInstance() {
14 return new OuterObjects();
15 }
16
17 public void func() {
18 System.out.println("OuterObjects.func");
19 }
20 }
21
22 // MockOuterObjects.java
23 package com.instancefactory.demo;
24 public class MockOuterObjects implements BaseObjects {
25 public static BaseObjects getInstance() {
26 return new MockOuterObjects();
27 }
28
29 public void func() {
30 System.out.println("MockOuterObjects.func");
31 }
32 }
33
34 // LogicToBeTested.java
35 packagecom.instancefactory.demo;
36 public class LogicToBeTested {
37 public static final String PROPERTY_KEY= "BaseObjects";
38 public void doSomething() {
39 // load configuration file and read the implementation class name of BaseObjects
40 // read it from properties to simplify the demo
41 // actually, the property file reader can be implemented by InstanceFactory
42 String impl = System.getProperty(PROPERTY_KEY);
43 InstanceFactory factory = new InstanceFactory(impl);
44 BaseObjects b = (BaseObjects)factory.getInstance();
45 b.doSomething();
46 }
47 }
48
49 // LogicTest.java
50 packagecom.instancefactory.demo;
51 importjunit.framework.TestCase;
52 public class LogicTest extends TestCase {
53 LogicToBeTested c;
54 protected void setUp() {
55 // set the property file of class map to a file for MockObjects, omitted
56 // use System.setProperty to simplify the demo
57 System.setProperty(LogicToBeTested.PROPERTY_KEY,
58 "com.instancefactory.demo.MockOuterObjects");
59 c = new LogicToBeTested();
60 }
61
62 public void testDoSomething() {
63 c.doSomething();
64 }
65 }