【IT168 技术文档】
写程序的时候,有时可能遇到这样的情况。比如我们开发了一个数据处理模块,被处理
的数据需要调用其他模块(由其他团队开发,或者是第三方提供,总之测试的责任不在你),
从数据库或者文件或者通过网络从其他进程中获取。为了对该数据处理模块进行单元测试,
我们通常得相应的配置起一个数据库或者文件系统,甚至是相关进程,以求正常的得到数据,
但这样做的代价往往较大。
这里想讨论一种我以前曾经使用过的简化单元测试的思路。通过接口来封装对外部模块
的调用,在单元测试时,用调试实现代替外部实现。受myworkfirst指点,又google了一
下,才知道这是单元测试里早已成熟的“测试桩”。但我仍然想把我的实践和大家分享一下。
我们用一个简单的例子来说明。比如我实现了一个SystemTimeSynchronizer 类,周期性
的查询NTP 标准时间,和本地系统时间进行比较。
/**shannon.demo is the package for the demonstration, in which,
* there's all the codes of unit test target module.
*/
package shannon.demo;
import thirdparty.any.NtpClock;
/**
* SystemTimeSynchronizer
is our unit test target,
* which acts as if calibrating the system time firmly in
* compliance with the standard time.
* @author Shannon Qian
*/
public class SystemTimeSynchronizer {
/**Compares the local system time with the standard time.
* @return - 1 if system time is ahead of standard time,
* 0 if it's on standard time and -1 if it's behind standard
* time.
*/
public int syncTime() {
long currentTime = new NtpClock().getTime();
long interval = System.currentTimeMillis()-currentTime;
if(interval == 0) {
return 0;
} else if(interval > 0) {
return 1;
} else {
return -1;
}
http://www.51testing.com
}
}
SystemTimeSynchronizer#syncTime() 调用的NtpClock 类, 属于外部模块。
NtpClock#getTime()在这里只是一个示意,说明在没有预设NTP 服务器的情况下,它将抛出
异常(这和我们在单元测试时实际遇到的情况类似)。但是请你想象其内部实现通过访问预设
的NTP 服务器获取标准时间。要让NtpClock 类正常的运行起来,需要一个NTP 服务器,
并事先进行比较复杂的设置。
/**package thirdparty.any plays the role to contain all the codes
* as if from thrid party.
*/
package thirdparty.any;
/**
* NtpClock
is a demenstrating class for this unit test
firewall
* example. it acts as if a third-party-provided adaptor with access
to the
* NTP server.
* @author Shannon Qian
*/
public class NtpClock {
/**Returns the standard time from NTP server.
* @return - the standard time from NTP server
* @throws IllegalStateException - if there's no NTP server
available.
*/
public long getTime() {
//if there's no NTP server available.
throw new IllegalStateException("NTP server is not ready.");
}
}
在不配置NTP 服务器的情况下,单元测试肯定会因为异常抛出而中断。为了避免麻烦,
我们首先想到的是如果不调用NtpClock 就好了。但如果不调用,就无法获取标准时间。这
样我们只能另外造一个类,在单元测试时替代NtpClock,能够方便的提供标准时间。新的
问题是SystemTimeSynchronizer 需要知道在不同时机调用不同的对象-在单元测试时,调用
我们自定义的类,而在正常运行时仍然调用NtpClock.
首先定义一个Clock 接口。并为Clock 实现两个具体类,一个是NtpClockWrapper,顾
名思义其实就是实现了Clock 的NtpClock,另一个是SystemClock,它就提供系统当前时间
作为标准时间。
package shannon.demo;
/**
* Clock
is an interface for all the clock to provide
time.
* @author Shannon Qian
*/
public interface Clock {
/**Returns the time in millusecond.
* @return - the time in millusecond
*/
public long getTime();
}
同时,我们定义一个UnitTestFirewall 类,维护一个debugging 标记。并提供一个getClock()
类工厂方法,返回Clock 对象。
package shannon.demo;
import thirdparty.any.NtpClock;
/**
* UnitTestFirewall
is the facility to
* ease unit test
* @author Shannon Qian
*/
public final class UnitTestFirewall {
private static boolean debugging=false;
/**Returns true if it's in debugging mode, else false.
* @return the debugging
*/
public static boolean isDebugging() {
return debugging;
}
/**Sets Debugging flag as true if it's time to unit test.
* @param on - the debugging to set, true for on and false
* for off
*/
public static void setDebugging(boolean on) {
UnitTestFirewall.debugging = on;
}
private final static NtpClock _ntpClock=new NtpClock();
private static class NtpClockWrapper implements Clock {
public long getTime() {
return _ntpClock.getTime();
}
}
private static class SystemClock implements Clock {
public long getTime() {
return System.currentTimeMillis();
}
}
private static SystemClock sysClock = null;
private static NtpClockWrapper ntpClock = null;
/**Returns the Clock instance for SystemTimeSynchronizer
's invocation.
*
* @return - Clock instance
*/
public static Clock getClock() {
if(debugging) {
if(sysClock == null)
sysClock = new SystemClock();
return sysClock;
} else {
if(ntpClock == null)
ntpClock = new NtpClockWrapper();
return ntpClock;
}
}
}
相应的SystemTimeSynchronizer#syncTime()也做了修改,不再直接调用NtpClock。
package shannon.demo;
import thirdparty.any.NtpClock;
public class SystemTimeSynchronizer {
public int syncTime() {
//long currentTime = new NtpClock().getTime();
long currentTime = UnitTestFirewall.getClock().getTime();
long interval = System.currentTimeMillis()-currentTime;
if(interval == 0) {
return 0;
} else if(interval > 0) {
return 1;
} else {
return -1;
}
}
}
在正常运行时,debugging 为false,SystemTimeSynchronizer 调用的是NtpClockWrapper
实例。在单元测试时,可以在测试代码里将debugging 设为true,这样SystemTimeSynchronizer
实际调用的是SystemClock 实例。
/**shannon.demo.unittest is the unit test codes
* package for the demonstration.
*/
package shannon.demo.unittest;
import shannon.demo.SystemTimeSynchronizer;
import shannon.demo.UnitTestFirewall;
import junit.framework.TestCase;
/**SystemTimeSynchronizerTest
is the
* unit test class for SystemTimeSynchronizer
* @author Shannon Qian
*/
public class SystemTimeSynchronizerTest extends TestCase {
protected void setUp() throws Exception {
super.setUp();
UnitTestFirewall.setDebugging(true);
}
protected void tearDown() throws Exception {
super.tearDown();
}
public void testSyncTime() {
int result = new SystemTimeSynchronizer().syncTime();
assertTrue(result==0||result==1||result==-1);
}
}
另一方面,其实我们也看到了,如果NtpClock 对象后面连着真正的NTP 服务器,那么
它永远只能返回正确的时间,而SystemTimeSynchronizer#syncTime()实际上提供了系统时间
快于、慢于和恰好等于标准时间三种情况的处理逻辑。如果要测全三种场景,修改NTP 服
务器会是件麻烦的事情。或者为此等很长时间,还要看运气。所以目前为止,我们只能在
SystemTimeSynchronizerTest#testSyncTime()里验证SystemTimeSynchronizer
#syncTime()的返回值是{0, 1, -1}中的任何一个。
要方便的测试所有的处理逻辑分支,关键就是要能够随心所欲的控制Clock#getTime()
的返回值。在此定义一个DebugClock 替换原来的SystemClock 类。
package shannon.demo;
/**
* DebugClock
is a Clock for debugging.
* It accepts arbitrary value as candidate time for
* the next return of {@link #getTime()}
* @author Shannon Qian
*/
public class DebugClock implements Clock {
private long time=-1L;
/**Sets candidate time for debugging.
* @param t - the candidate time value in millisecond
* @see #getTime()
*/
public void setTime(long t) {
this.time=t;
}
/** Returns the time in millisecond.
* By default, it returns system time if the
* candidate time is not preset. else it will
* return the candidate time after reseting it.
* @return - time in millisecond
* @see #setTime(long)
*/
public long getTime() {
if(time<0L)
return System.currentTimeMillis();
long t=this.time;
this.time=-1L;
return t;
}
}
在UnitTestFirewall 中也要做相应的修改,以允许在测试时,定制Clock 实例。
package shannon.demo;
import thirdparty.any.NtpClock;
/**
* UnitTestFirewall
is the facility to
* ease unit test
* @author Shannon Qian
*/
public final class UnitTestFirewall {
private static boolean debugging=false;
/**Returns true if it's in debugging mode, else false.
* @return the debugging
*/
public static boolean isDebugging() {
return debugging;
}
/**Sets Debugging flag as true if it's time to unit test.
* @param on - the debugging to set, true for on and false
* for off
*/
public static void setDebugging(boolean on) {
UnitTestFirewall.debugging = on;
}
private final static NtpClock _ntpClock=new NtpClock();
private static class NtpClockWrapper implements Clock {
public long getTime() {
return _ntpClock.getTime();
}
}
private static NtpClockWrapper ntpClock = null;
private static Clock clock = null;
/**sets the Clock instance for debugging.
* If candidate is not null, {@link #getClock()} will
* return it when debugging is true.
* @param candidate - the Clock instance for debugging
*/
public static void presetClock(Clock candidate) {
clock=candidate;
}
/**Returns the Clock instance for SystemTimeSynchronizer
's invocation.
*
* @return - Clock instance
*/
public static Clock getClock() {
if(debugging && clock != null) {
return clock;
} else {
if(ntpClock == null)
ntpClock = new NtpClockWrapper();
return ntpClock;
}
}
}
接着,SystemTimeSynchronizerTests 就可以作很大的改进,通过预设标准时间,测试
SystemTimeSynchronizer#syncTime()的各个处理分支了。
/**shannon.demo.unittest is the unit test codes
* package for the demonstration.
*/
package shannon.demo.unittest;
import shannon.demo.DebugClock;
import shannon.demo.SystemTimeSynchronizer;
import shannon.demo.UnitTestFirewall;
import junit.framework.TestCase;
/**SystemTimeSynchronizerTest
is the
* unit test class for SystemTimeSynchronizer
* @author Shannon Qian
*/
public class SystemTimeSynchronizerTest extends TestCase {
protected void setUp() throws Exception {
super.setUp();
UnitTestFirewall.setDebugging(true);
UnitTestFirewall.presetClock(debugClock);
}
protected void tearDown() throws Exception {
super.tearDown();
}
private static DebugClock debugClock=new DebugClock();
public void testSyncTime() {
SystemTimeSynchronizer sync=new SystemTimeSynchronizer();
assertTrue(sync.syncTime()==0);
debugClock.setTime(System.currentTimeMillis()-10L);
assertTrue(sync.syncTime()==1);
debugClock.setTime(System.currentTimeMillis()+10L);
assertTrue(sync.syncTime()==-1);
}
}
再总结一下这种测试桩方法的好处,我体会到的有两个:
1) 免去许多配置和调试外部模块的工作;
2) 可以方便的模拟其他模块的各种行为,提供各种场景测试条件。
当然,实际问题要复杂的多,所以我们可能要比这个例子做得更多。但正因为实际问题复杂,
我们省的力气也要多得多。
另外有同事告诉我JMock 和easyMock 也提供和我上面所讨论的法子类似的功能。我花
了极少的时间试着了解过JMock,但还没入门,所知很少,还望有经验的同行指教。再次感
谢myworkfirst。