世界级的安卓测试开发流 — 第二部分

在前一篇博文世界级的安卓测试开发流 — 第一部分",我们开始讲安卓的测试开发流.我们看到一个软件工程师一度开始写测试,然后发现关于测试开发相关的问题的过程.我们最后做出来些结论,总结于此:

  • 测试自动化是成功的软件开发之关键
  • 可测的代码对写某些特定类型的测试是必须的
  • 有些开发者在测什么和怎么测未知的情况下开始写测试
  • 我们测试的质量和可靠度通常达不到我们希望的那样
  • 一个测试开发流(程)需要定义测些什么和怎么测试它们

相应的,在任一应用程序中测试的关键部分是:

  • 业务逻辑的测试要独立于框架或库
  • 测试集成的服务器端API
  • 验收标准测试要从使用者的视角用黑盒的方案

这篇博文,我们将回顾覆盖前面谈到的各部分的几种测试方案,以确保测试开发流(程)坚如磐石.

业务逻辑测试独立于框架或库:

首先,检查业务逻辑是不是真的实现产品预定的需求很有必要.我们需要隔离出想要测试的代码,然后模拟出不同的出事场景,以配置某些组件运行时的行为.然后,我们选择想要执行的代码部分进行测试.一旦完成这些,最后,我们需要在执行完测试项后检查软件的状态是正确的.

这个测试方案的关键在于依赖反转原则.通过写只依赖于抽象(接口)的代码,我们就可以把软件分离称不同的层级.为了获得依赖的实例,我们只需要向某人(或某角色的组件)请求.或者,我们能获得已经生成的实例.我们的软件有很多部分需要创建代码来获得协作者的实例.在这些个点上,我们引入test double来模拟出事场景或程序的不同行为来设计我们的测试.使用test doubles,我们能替换test double同时模拟产品代码的行为和状态.同时,它帮助我们选择测试的范围边界,这主要表达要测试的代码量.没有依赖反转,所有的类都自顾自的获取自己的依赖.结果是类实现深深的耦合了他所依赖项的具体实现,这样我们就没有办法借用test doubles来切割产品的代码执行流程.

通常在构造函数传入依赖项是使用依赖反转的最有效的机制.这个机制对采用test doubles足够好用.在构造函数传入依赖项将帮助我们创建用相应的test doubles替换依赖项的实例.要知道的很重要的一点是使用服务分配或依赖反转框架将有助于去掉在应用依赖反转时的所有边角代码,虽然这些框架不是必选项.

我们将用一个具体例子(测试关于我几个月前开始工作的Android GameBoy 仿真器)来演示如何测试我们的业务需求

以下测试是关于GameBoy内存管理单元和GameBoy BIOS执行单元的.我们打算检查我们的产品需求(硬件模拟)已经正确实现.


public class MMUTest {  
private static final int MMU_SIZE = 65536;
private static final int ANY_ADDRESS = 11;
private static final byte ANY_BYTE_VALUE = 0x11;

@Test public void shouldInitializeMMUFullOfZeros() {
MMU mmu = givenAMMU();

assertMMUIsFullOfZeros(mmu);
}

@Test public void shouldFillMMUWithZerosOnReset() {
MMU mmu = givenAMMU();

mmu.writeByte(ANY_ADDRESS, ANY_BYTE_VALUE);
mmu.reset();

assertMMUIsFullOfZeros(mmu);
}

@Test public void shouldWriteBigBytesValuesAndRecoverThemAsOneWord() {
MMU mmu = givenAMMU();

mmu.writeByte(ANY_ADDRESS, (byte) 0xFA);
mmu.writeByte(ANY_ADDRESS +1, (byte) 0xFB);

assertEquals(0xFBFA, mmu.readWord(ANY_ADDRESS));
}
}

开始三个测试检查GameBoy的MMU的实现是正确的.成功的关键是总在执行完测试检查MMU的状态是正确的.所有测试检查MMU是否正确初始化.重置后MMU是干净的,写入2个字节后读出的是一个word,最终读取是正确的.要测试这个仿真软件的这个部分我们选择一个范围限定到一个类作为测试主题来测.


public class GameBoyBIOSExecutionTest {

@Test
public void shouldIndicateTheBIOSHasBeenLoadedUnlockingTheRomMapping() {
GameBoy gameBoy = givenAGameBoy();

tickUntilBIOSLoaded(gameBoy);

assertEquals(1, mmu.readByte(UNLOCK_ROM_ADDRESS) & 0xFF);
}

@Test
public void shouldPutTheNintendoLogoIntoMemoryDuringTheBIOSThirdStage() {
GameBoy gameBoy = givenAGameBoy();

tickUntilThirdStageFinished(gameBoy);

assertNintendoLogoIsInVRAM();
}

private GameBoy givenAGameBoy() {
z80 = new GBZ80();
mmu = new MMU();
gpu = new GPU(mmu);
GameLoader gameLoader = new GameLoader(new FakeGameReader());
GameBoy gameBoy = new Gameboy(z80, mmu, gpu, gameLoader);
return gameboy;
}

}

这两个测试检查BIOS在不同阶段正确执行.BIOS执行结束时,在具体内存位置的一个字节必然被初始化为一个具体的值.然后在第三阶段,Nintendo的logo已经加载到VRAM.这个测试用例的测试主题是CPU,CPU指令集(仅涉及BIOS执行的指令)和MMU.要检查执行的状态是正确的,我们必须对MMU的状态进行断言.要想戏剧性的提升测试的质量,一个关键就是检查软件执行后的最终状态,避免验证何和其他组件交互.因为即便和其他组件的交互全都正确,最终状态还可能会是错的.还要重点明确这些测试的一些部分同样可以独立进行测试,如CPU指令.

这些测试的另一个主要亮点是使用test double来模拟Android SDK相关代码部分.在执行BIOS前,GameBoy游戏必须加载进GameBoy的MMU.然而在测试的时候,还没Android SDK可用,因而就不得不替代掉它转而从测试环境加载GameBoy的rom.*我们使用依赖反转原则不仅局限于隐藏实现细节,或者定义(测试)边界,*同样用来替换产品代码AndroidGameReader为一个test double的FakeGameReader,这意味着完全不依赖框架和库进行代码测试.如此这般,我们创建了隔离的测试环境也调整了测试的范围

范围:

调整测试的范围超级重要.开始写测试前我们应当牢记测试范围可以帮助我们标识我们失效的代码(依赖于测试范围的大小).简化的测试范围能给我们丰富的错误反馈而放大范围的测试不能提供定位bug的精确信息.测试的粒度当尽可能小到在考虑测试的范围内

基础设施(Infrastructure):

写这些测试的基础设施相当明了.我们需要写依赖反转原则下的可测代码并使用一个测试框架搭配一个模拟库.模拟库用来生成模拟场景或者替换某些产品代码部分的test doubles.注意,使用这些框架和库不是必须的,但是非常推荐.

结果:

这个方案的结果很有趣.遵循了依赖反转原则后我们能独立于框架和库测试我们产品代码中的业务逻辑.重用易于编写和设计的测试我们可以创建隔离的测试环境,另外,我们能很容易选择要测试的产品代码数量并把这部分代码用test doubles替换来模拟不同场景的行为

一旦我们可以测试我们的产品需求实现正确,我们需要继续搞我们的测试开发流(程),我们接下来的事情就是要测试我们和之前被test doubles替换的外部组件集成后是否正确.这是下一篇博文要谈到的,敬请期待!;)

参考: