我们以智能家居为例,一所房子里面有房间和大门,房间包括卧室、厨房等。房间是房子的内部类,并且是在房子的构造函数中初始化的,对外没有暴露接口。
代码示例1:房子类
public class House
{
private Bedroom bedroom;
private Kitchen kitchen;
private FrontDoor door;
public house() {
bedroom = new Bedroom();
kitchen = new Kitchen();
door = new FrontDoor();
}
//出门
public void leaveHouse() {
//关闭厨房电器
kitchen.shutdownAllAppliances();
//关闭卧室电灯
bedroom.turnLightOff();
//关闭大门
lockFrontDoor();
}
//检查大门是否关闭
boolean isFrontDoorLocked(){
return door.isLocked();
}
private void lockFrontDoor(){
door.lockDoor();
}
}
我们编写当离开房子时大门是否关闭的测试案例。
案例举例如下:
代码示例 2:测试离开房子时大门是否关闭。
public class HouseTest{
private House house;
public void testLockFrontDoor(){
house = new House();
house.leaveHouse();
assertTrue(house.isFrontDoorLocked());
}
}
当我们测试 House 类的时候,如果使用房间类(Bedroom、Kitchen)的一些功能,可能会有些问题。在很多情况下 House 类的内部逻辑可能很复杂,它也可能依赖其他类的复杂功能。例如,House 类是否会依赖硬件控制类。但是我们的测试环境可能并没有硬件控制的测试环境,在这种情况下测试类将无法运行,或者由于依赖环境的缺失一直测试执行失败。
很明显,以上这个 House 类的可测试性就不好。可测试性架构设计建议我们利用接口把类从依赖环境中解耦。被依赖实例通过依赖注入方式完成。
在我们的单元测试中,我们要验证 House 的调用逻辑,而不需要真实的调用执行真实的关闭电器、关闭电灯等功能。我们模拟离开房子的过程,然后验证大门是否关闭了。我们需要通过创建模拟类来代替实际类,测试时我们把模拟类注入到 House 实例中完成被测试逻辑。
不幸的是当前 House 类并没有提供注入模拟测试类的机制,House类没有对外暴露内部成员类的方法,也没有机制来替换实际的成员类。
解决成员变量类初始化的一个方法是利用依赖注入(Dependency Injection,DI),在 House 类外面对成员类进行初始化。我们首选把 Kitchen 类和 BedRoom 类换成接口(Interface),然后使用 House 类构造函数注入成员变量。
代码示例 3:House 类重构
public class House
{
private IBedroom iBedroom;
private IKitchen iKitchen;
private FrontDoor door;
//
public house(IBendRoom iBedRoom,IKitchen iKitchen) {
this.iBedroom = iBedroom;
this.iKitchen = iKitchen;
door = new FrontDoor();
}
//出门
public void leaveHouse() {
//关闭厨房电器
iKitchen.shutdownAllAppliances();
//关闭卧室电灯
iBedroom.turnLightOff();
//关闭大门
lockFrontDoor();
}
//检查大门是否关闭
boolean isFrontDoorLocked(){
return door.isLocked();
}
private void lockFrontDoor(){
door.lockDoor();
}
}
测试代码中使用依赖注入机制利用模拟类进行测试,从而避免对真实环境的依赖。
代码示例 4:依赖注入和接口注入模拟类
public class HouseTest{
//Bedroom 模拟类
private FakeBedroom fakeBedroom;
//Kitchen 模拟类
private FakeKitchen fakeKitchen;
private House house;
public void testHouse(){
fakeBedroom = new FakeBedroom();
fakeKitchen = new FakeKitchen();
house = new House(fakeBedroom,fakeKitchen);
house.leaveHouse();
assertTrue(house.isFrontDoorLocked());
}
}
为了能正确模拟 Bedroom 和 Kitchen 的行为,FakeBedroom 和 Bedroom 应该实现 IBedRoom 接口,FakeKitchen 和 Kitchen 应该实现 IKitchen 接口。
我们当前的代码示例目的就是把被测试类 House 从依赖类中剥离出来。如果不隔离依赖类,相关被测试目标类将很难测试,以后的代码维护也变得非常困难。
验证两个对象之间的交互是否正确是常见的一种测试。在示例代码 1 中,我们是为了验证当离开房子时,相关离开流程是否正确执行,例如厨房的电器设备应该关闭等。比较明智的设计应该是,House 类向 Kitchen 类发送关闭电器的指令,Kitchen 类负责关闭厨房电器。请见示例代码 4。
为了模拟 Kitchen 的行为,我们需要在 FakeKitchen 类中增加一些验证逻辑。FakeKitchen 类需要实现 IKitchen 接口,以便可以使用依赖注入(DI)。
示例代码 5: FakeKitchen类
public class FakeKitchen implements IKitchen{
private boolean shutDownAllAppliancesFlag;
public FakeKitchen(){
this.shutDownAllAppliancesFlag = false;
}
boolean wasCalledShutDownAllAppliances(){
return this.shutDownAllAppliancesFlag;
}
public void shutDownAllAppliances(){
this.shutDownAllAppliancesFlag = true;
}
}
敏捷测试社区公众: