上周末去听了Scrumgathering的试讲活动,感觉此类活动还是挺有意思的,一群scrum实践者或者爱好敏捷的同学在一起讨论如何做好敏捷项目,这次主要听了一场关于如何对遗留代码做单元测试的演讲,因此向记录一下一些很好的观点,来用于我们对单元测试的理解,以及如何提高代码可测性。

所谓的遗留代码(legacy code),简单就是指没有任何测试的代码。那么我们如何来对这些遗留代码进行测试,或者是通过修改使这些遗留代码能够变的更加testability。

案例一、

public class Car{

private Engine engine;

public Car(){

engine = new Engine(10);

}

public boolean isMove(){

  return engine.speed()>0? true:false;

}

}

针对这个class的test case应该是这样的:

public class TestCase{

private Car car;

public void testMove(){

 car = new Car();

Assert.assertEquals(true,car.isMove());

}

}

对于这个测试用列,我们并没有真正的达到测试Car的isMove方法的正确性,因为Car在这里对Engine类的一个依赖,在这个测试用列中我们没法达到测试isMove方法中的逻辑。因此我们需要对Car这段遗留代码进行解依赖。

对Car这个类的构造函数进行修改

public class Car{

private Engine engine;

public Car(Engine engine){

this.engine = engine;

}

public boolean isMove(){

return engine.speed()>0?true:false;

}

}

测试代码如下:

public class TestCase{

public Car car;

public void testIsMove(){

Engine engine = new MockEngine(10);

car = new Car(engine);

Assert.assertEquals(true,car.isMove());

}

}

可以看到这时的测试代码已经完完全全的在测试isMove方法了,我们不需要关心Engine,我们可以随便mock一个engine对象。这样就完成了对Engine的解依赖,使代码具有可测性。

案例二、

提高方法的可见性,如果实际工作中我们需要对一个private的方法进行测试的话,我们可能会无从下手,对于这类测试,我们可以说是该方法无法在测试工具中导入,那么这个时候,我们需要提升一下方法的权限,比如我们可以将private方法改为protected,那么我们就可以通过子类继承被测类,子类中对protected方法是可见的,那么我们就可以将子类导入到测试用具中完成测试工作。

案例三、

虽然目前已经有很多的mock工具,如jmock, easymock等工具,这些工具可以很方便的构造mock类,但是这些类的可读性会比较差,因此提倡自己写mock类,这样不但可以增加可读性,并且对于mock类的实现我们可以比较自由。

案例四、

对于较大类的解决,在实际工作中我们可能会遇到一个非常庞大的类,这个类可能有几百个方法,那么如何提高这种类的可测性,提倡一个原则(单一职责原则SRP),每个类应该仅承担一个职责:它在系统中的意图应当是单一的,且修改它的原因应该只有一个。

案例五、

如果需要在遗留代码中添加一些新的功能,一些非常简单的功能的时候我们应该怎么做,如下代码,我们需要在parse方法之后加上日志记录,那么我们可以通过以下几种方法来做:

public class Parser{

public void parse(){

  .........

}

}

我们可以添加一个方法叫做parseWithLogger().

如:

public class Parser{

 public void parseWithLogger(){

    Logger loger = Logger.log(.....);

    parse();

}

 public void parse(){

 ......

}

}

这样我们不需更改原来的parse方法进行修改,我们之需要增加一个新的方法,然后在我们测试用具中对这个新的方法进行测试就行。

我们也可以这样更改代码,把原方法改名为parseWithoutLogger().

public class Parser{

 public void parseWithoutLogger(){

   ...........

}

public void parse(){

  Logger log = Logger.log();

  parseWithoutLogger();

}

}

这样对代码的改动也不是很大,我们可以在测试用具中测试parse方法是否加logger。

两种方法对遗留代码都不会造成大改变,我们强调对遗留代码的修改一定要小心,因为遗留代码是没有测试代码,我们不能大刀阔斧的进行修改,只能小步前进,然后对遗留代码加上测试用列,保证重构的正确性。

当然对遗留代码的可测性提高还有很多方法,可以参考Michale Feathers 写的<working effectively with legacy code>.同时也非常感谢@姚若舟 周末分享的How to write unit test for new code based on legacy code。 非常精彩的演讲。