稍微看了一下这段代码实现会发现这个系统是难以测试的,为什呢?因为内存中的K-V对会缓存一个小时,如果要测试缓存时间1个小时的逻辑对不对,那么要等上1个小时才可以,这显然是不可能的。System.currentTimeMillis() 是JDK中一个的方法,用来获取当前时间的毫秒值,这里我用来获取当前时间以便判断是否超出1个小时。问题出在这里,如果能够让事件变得可控制,那么不容易测试了吗?于是有了新的设计:
// 设计时间接口
public interface SystemClock {
public long getCurrentTime();
}
// 业务代码中的实现
public class SystemClockImpl implements SystemClock {
@Override
public long getCurrentTime() {
return System.currentTimeMillis();
}
}
// 改进后的 ShortLifeMemoCache<T>
public class ShortLifeMemoCache<T> implements CacheService<T> {
private static final long DEFAULT_TTL = 1L*60*60*1000;
/**
* 系统时间,单位:毫秒
*/
private SystemClock systemClock;
/**
* 生存时间(Time to live),单位:毫秒
*/
private long ttl = DEFAULT_TTL;
private final Map<String, Value<T>> cache =  new HashMap<String, Value<T>>();
@Override
public void put(String key, T t) {
long life = systemClock.getCurrentTime() + ttl;
cache.put(key, new Value<T>(t, life));
}
@Override
public T get(String key) {
Value<T> v = cache.get(key);
if (v != null){
if(v.getLife() > systemClock.getCurrentTime()) {
return v.getValue();
} else {
cache.remove(key);
}
}
return null;
}
}
  有了这样的设计可以随意的控制时间了,比如我们的单元测试代码可以写成这样(并且瞬间执行完测试用例):
// 测试用例:设置ttl=10,时间由FakeSystemClock控制
public void testValueTimeout() {
long timeToLive = 10;
FakeSystemClock systemClock = new FakeSystemClock();
systemClock.setCurTime(0);
ShortLifeMemoCache<String> cacheService = new ShortLifeMemoCache<String>();
cacheService.setSystemClock(systemClock);
cacheService.setTtl(timeToLive);
cacheService.put("key1", "value1");
// after 10 seconds
systemClock.setCurTime(10);
assertNull(cacheService.get("key1"));
}
// Mock 时间接口
static class FakeSystemClock implements SystemClock {
private long curTime;
@Override
public long getCurrentTime() {
return curTime;
}
public long getCurTime() {
return curTime;
}
public void setCurTime(long curTime) {
this.curTime = curTime;
}
}
  这种设计是我们大家熟悉的 - 面向接口编程!我们把时间的获取抽象成一个接口,这样可以在测试时不再依赖于物理时间。事实上在我们的程序设计中,对于一切需要可控的依赖都要设计成接口,这样系统才会变得容易测试。经常在一些Android开发群里看到大家在抱怨MVP模式的复杂 - 其中一点是说MVP用了很多接口,把View和Presenter都抽象成了接口。而事实情况是MVP从来都没有要求使用过接口,MVP模式在90年代已经出现了,10年后才有面向接口编程相关书籍出版,而现在大家能看到的MVP使用了大量接口是因为测试!这种MVP实现全称叫PassiveView-MVP,也是目前用的多的MVP模式(具体见:UI架构小史3)。
  在一个容易测试的系统中必然会大量使用接口,然而这并不能解决所有问题,有些测试依然很难,比如需要Mock一些系统库提供的API,这时候可以使用一些Mock工具,比如 EasyMock,mockito 都是很好用的Mock工具,跑单元测试当然离不开jUnit,UI相关还可以使用 Robotium 等.
  对于Android的UI测试还需要多说几句,因为大部分时候我们还会把UI布局/效果和UI/业务逻辑混淆了,这样会导致测试很难进行。通常情况下需要把逻辑(无论是UI逻辑还是业务逻辑)抽象到Model里面(Model是个动词,意指ModelingTheWorld),Model不依赖于系统框架容易测试。
  PS:依赖注入
  在上面的方案中我们默认省略了 getter 和 setter 方法的代码,但是这是必须要有的。我们通常会听到一个名词叫“依赖注入”,那么什么是依赖注入呢?看下面这段代码:
public class Foo {
private String message = "Hello World";
private String View view = new View();
public void print() {
System.out.println(message);
}
public void showMessage() {
view.display(message);
}
}
  这段代码中 message 和 view 变量在Foo初始化的时候确定了,无法再改变 - 这是典型的 依赖无法注入 的例子。解决办法有很多种,比如:
//提供一个可以传参的构造方法
publicFoo(Stringmessage,Viewview){
this.message=message;
this.view=view;
}
//或者提供setter方法
publicvoidsetMessage(Stringmessage){
this.message=message;
}
publicvoidsetView(Viewview){
this.view=view;
}
  上面这两者都是典型的依赖注入的实现,关于依赖注入可以看很早前写过的一篇文章:依赖注入,还有很多比较自动化的基于Ioc的实现方式,比如Spring提供的依赖注入框架或者是这几年在Android届比较流行的Dagger框架。