[b]问题:[/b]
你想测试一个对象,但这个对象内部还初始化其他对象,这使得测试变得困难
[b]背景:[/b]
面向对象的设计是双刃剑。我们使用聚合来表明一个对象拥有另外一个对象,比如大多数情况下一个汽车都有自己的轮子。而另一方面,为了单独测试一个对象,我们需要将对象像拼图游戏一样拼凑起来。这就是说,测试更期望对象使用组合而不是聚合。如果你要测试的对象初始化了其他的对象,那么只有在保证内部对象的正确性的前提下,你才能测试这个对象,而这就违反了单独测试一个对象的原则。
我们不安地发现,仍然有大量的程序员对进行测试的好处一无所知。这致使他们过多地使用聚合。他们的设计中充满了初始化其他对象的对象,或者常常从全局的位置获取对象。这种编程办法,如果不改进的话,就会造成强耦合的架构,导致测试难以进行。我们知道:我们继承了大量这种设计,甚至直接创建了它们。下面的诀窍介绍了一个基本的测试技巧,它可以带来程序设计上的一些提高,是单独的测试一个对象成为可能。
[b]诀窍:[/b]
为了处理这个问题,你要将要测试的对象的内部对象用其他的对象代替。这就有两个问题需要解决:
1. 如何创建这个对象所包含的对象的替身
2. 如果将它传递给要测试的对象
为了简化讨论,我们使用术语“测试对象”(不要与对象测试相混淆),用来指代那些你要测试的类或者接口的实例。有多种不同的测试对象:假的、残缺的、模拟的。
创建一个接口的测试对象很简单:只要创建一个类,并用最简单的方法实现这个接口就可以了,这是最简单的测试对象。在这里我们使用EasyMock(www.easymock.org)来创建接口的测试对象。之所以使用这个包,是因为它不仅可以避免我们重复的劳动,还能保证我们仿造的接口对象的一致性和统一性。这让我们的测试程序更容易理解。
创建一个类的测试对象,可以创建它的一个子类,然后仿造它的全部方法,或者干脆屏蔽它的全部方法。一个仿造的方法返回某种可预见的、有意义的、硬编码的值,而一个屏蔽的方法不做任何有意义的事情,只需要实现编译要求即可。你也许会发现:在要测试的对象中声明接口,然后在测试时转变为具体类的对象是很有好处的。因为这样你就可以使用前面介绍的EasyMock了。此外,如果你在对象中要使用其他的对象,最好将其声明为接口,这样你的设计就更有弹性。
至于第二个问题,我们有两种方法可以让你将一个测试对象传递给另一个要测试的对象:一种是改造构造函数,另一种是添加一个set方法。我们认为改造构造函数的方法比较简单,这样可以避免在测试中再去调用set方法。为了说明,可以看个例子
注意,Deployment使用类级别的方法Deployer.getInstance()来生成Deployer。如果你想仿造一个Deployer,你需要将一个Deployer通过某种方法传递给Deployment。我们推荐使用构造函数来传递,所有新加一个构造函数并创建一个实例来保存Deployer;
怎么好像没看见Deployer.getInstance()方法?我们不能丢掉这段代码:现在我们删除了无参数的构造函数,那么我们需要添加一个新的构造函数。
现在,当产品代码使用无参构造函数创建一个Deployment时,就可以观察到期望的行为:Deployment将使用Singleton Deployer。不过,在我们的测试中,我们使用一个假的Deployer,这样做可以模拟诸如目标文件不存在等情况。下面的代码是一个“故障测试”的Deployer------它永远都认为目标文件不存在
现在,我们可以测试当Deployer部署失败的时候,Deployment如何表现。
这个测试演示了如何替换一个对象中引用的测试对象,同时也介绍了如何通过继承仿制测试对象。我们将使用的对象变成了构造函数的一个可选参数:如果我们没有提供的话,那么类也会自动察觉,并创建一个默认的。
你想测试一个对象,但这个对象内部还初始化其他对象,这使得测试变得困难
[b]背景:[/b]
面向对象的设计是双刃剑。我们使用聚合来表明一个对象拥有另外一个对象,比如大多数情况下一个汽车都有自己的轮子。而另一方面,为了单独测试一个对象,我们需要将对象像拼图游戏一样拼凑起来。这就是说,测试更期望对象使用组合而不是聚合。如果你要测试的对象初始化了其他的对象,那么只有在保证内部对象的正确性的前提下,你才能测试这个对象,而这就违反了单独测试一个对象的原则。
我们不安地发现,仍然有大量的程序员对进行测试的好处一无所知。这致使他们过多地使用聚合。他们的设计中充满了初始化其他对象的对象,或者常常从全局的位置获取对象。这种编程办法,如果不改进的话,就会造成强耦合的架构,导致测试难以进行。我们知道:我们继承了大量这种设计,甚至直接创建了它们。下面的诀窍介绍了一个基本的测试技巧,它可以带来程序设计上的一些提高,是单独的测试一个对象成为可能。
[b]诀窍:[/b]
为了处理这个问题,你要将要测试的对象的内部对象用其他的对象代替。这就有两个问题需要解决:
1. 如何创建这个对象所包含的对象的替身
2. 如果将它传递给要测试的对象
为了简化讨论,我们使用术语“测试对象”(不要与对象测试相混淆),用来指代那些你要测试的类或者接口的实例。有多种不同的测试对象:假的、残缺的、模拟的。
创建一个接口的测试对象很简单:只要创建一个类,并用最简单的方法实现这个接口就可以了,这是最简单的测试对象。在这里我们使用EasyMock(www.easymock.org)来创建接口的测试对象。之所以使用这个包,是因为它不仅可以避免我们重复的劳动,还能保证我们仿造的接口对象的一致性和统一性。这让我们的测试程序更容易理解。
创建一个类的测试对象,可以创建它的一个子类,然后仿造它的全部方法,或者干脆屏蔽它的全部方法。一个仿造的方法返回某种可预见的、有意义的、硬编码的值,而一个屏蔽的方法不做任何有意义的事情,只需要实现编译要求即可。你也许会发现:在要测试的对象中声明接口,然后在测试时转变为具体类的对象是很有好处的。因为这样你就可以使用前面介绍的EasyMock了。此外,如果你在对象中要使用其他的对象,最好将其声明为接口,这样你的设计就更有弹性。
至于第二个问题,我们有两种方法可以让你将一个测试对象传递给另一个要测试的对象:一种是改造构造函数,另一种是添加一个set方法。我们认为改造构造函数的方法比较简单,这样可以避免在测试中再去调用set方法。为了说明,可以看个例子
public class Deployment{
public void deploy(File targetFile
throws FileNotFoundException{
Deployer.getInstance().deploy(this, targetFile);
}
}
注意,Deployment使用类级别的方法Deployer.getInstance()来生成Deployer。如果你想仿造一个Deployer,你需要将一个Deployer通过某种方法传递给Deployment。我们推荐使用构造函数来传递,所有新加一个构造函数并创建一个实例来保存Deployer;
public class Depliyment{
private Deployer deployer;
public Deployment(Deployer deployer){
this.deployer = deployer;
}
public void deploy(File targetFile)throws FileNotFoundException{
deployer.deploy(this, targetFile);
}
}
怎么好像没看见Deployer.getInstance()方法?我们不能丢掉这段代码:现在我们删除了无参数的构造函数,那么我们需要添加一个新的构造函数。
public class Deployment{
private Deployer deployer;
public Deployment(){
this(Deployer.getInstance());
}
public Deployment(Deployer deployer){
this.deployer = deployer;
}
public void deploy(File targetFile){
deployer.deploy(this, targetFile);
}
}
现在,当产品代码使用无参构造函数创建一个Deployment时,就可以观察到期望的行为:Deployment将使用Singleton Deployer。不过,在我们的测试中,我们使用一个假的Deployer,这样做可以模拟诸如目标文件不存在等情况。下面的代码是一个“故障测试”的Deployer------它永远都认为目标文件不存在
public class FileNotFoundDeployer extends Deployer{
public void deploy(Deployment deployment, File targetFile)throws FileNotFundException{
throw new FileNotFoundException(targetFile.getPath());
}
}
现在,我们可以测试当Deployer部署失败的时候,Deployment如何表现。
public void testTargetFileNotFound() throws Exception{
Deployer fileNotFoundDeployer = new FileNotFoundDeployer();
Deployment deployment = new Deployment(fileNotFoundDeployer);
try{
deployment.deploy(new File("hello"));
fail("Found target file?!");
}
catch(FileNotFoundException expected){
assertEquals("hello", expected.getMessage());
}
}
这个测试演示了如何替换一个对象中引用的测试对象,同时也介绍了如何通过继承仿制测试对象。我们将使用的对象变成了构造函数的一个可选参数:如果我们没有提供的话,那么类也会自动察觉,并创建一个默认的。