JUnit和它的设计模式

JUnit设计原则

先来说说单元测试框架应当遵循什么原则:

  1. Each unit test should run independently of all other unit tests.
  2. The framework should detect and report errors test by test.
  3. It should be easy to define which unit tests will run.

JUnit 设计目标:

  1. The framework must help us write useful tests.
  2. The framework must help us create tests that retain their value over time.
  3. The framework must help us lower the cost of writing tests by reusing code.

Junit是如何实现单元测试框架应当遵循的原则的:

  1. Separate test class instances and class loaders for each unit test to avoid side effects (目标1)
  2. JUnit annotations to provide resource initialization and reclamation methods: @Before, @BeforeClass, @After, and @AfterClass (目标3)

JUnit 基本概念

  1. Test Class (or TestCase), 是一组测试方法(@Test修饰的方法)的集合
  2. Suite (or TestSuite), The Suiteis a container used to gathertests for the purpose of grouping and invocation. 是一组测试逻辑上相关的测试方法的集合,默认情况下,JUnit将 Test Class 下的测试方法组织为一个 test suite。换而言之,TestSuite是组织测试方法的单位。 Suite默认情况下都会对test class中的 @Test 方法建立一个单独的test class 实例,也就是说如果你有10个 @Test 方法,那么就会有10个test class的实例,这是为了避免不同test方法之间产生相互作用。
  3. Runner (or TestRunner), A runner of test suites。Runner 是实际执行测试方法的主体。相当于每一个测试方法执行的上下文容器,执行包括创建测试方法、执行测试方法、报告测试方法的功能。

另外一个额外的概念JUnitCore,JUnitCore是JUnit提供的一个对Test Runner的门面(facade)

Junit @Before/@After@BeforeClass/@AfterClass 的区别:

  1. @Before/@After 会在test class 中的 @Test 方法前/后执行
  2. @BeforeClass/@AfterClass 会在test class中的所有 @Test方法前/后执行。 而且@BeforeClass/@AfterClass 需要时静态的。

设计模式

下面列举一些JUnit中使用的模式

组合模式

体现在Test Suite的组合上。Test Suite因为可以是测试方法的组合

Facade 模式

提供对多个Interface同一的使用接口

A facade is a design pattern that provides a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use. You can use a fa?ade to simplify a number of complicated object interactions into a single interface

JUnitCore 决定了采用哪种Runner来运行测试方法

题外话

如何优雅的测试

不在于使用多牛的工具,而在于写出便于测试的代码。测试正式自己代码的第一个用户,便于测试的代码同样也会便于用户使用

写出便于测试代码的准则:

不要轻易改变Public API

你的代码可能会被很多人使用,如果轻易的修改了公开的API,那么意味着很多你不清楚的外部调用的地方都要修改,这样的话API是不友善的

减少外部依赖

如果一个业务逻辑本身还要负责实例化它所依赖的类,那么不如将依赖类提供为一个借口,这样通过外部提供不同的mock对象,同时也就方便了测试

简化构造器同时保持对象简单

一个对象仅仅只需要关注他应该知道的,如果一个简单类的构造都需要传入复杂的外部上下文,那么测试是很麻烦的

少用全局变量(global state)

全局状态可能需要多个对象相互作用才能起作用,如果大量使用了全局状态,那么久不方便测试

多态优于静态

通常代码中如果具备多态可替换性,将有利于测试的时候采用测试代码测试目标逻辑。相比于静态的方法的时候,使用的对象类型编译期就确定了,这样的话也就不便于运行时替换对象类型。

采用多态意味着可以写出更多可以重用的代码,便于扩展,减少重复代码。

组合优于继承

组合和继承都是可以提升代码的重用度。但是继承在编译期就已经确定,运行时无法修改关系,而组合的话,可以运行时动态替换

多态优于条件判断

多态是面向对象提供的可以避免条件判断的基本工具。复杂的条件判断,当条件增加或者减少都需要维护 if-else 或者 switch-case,而且复杂的 if-else 或者 switch-case 是不方便测试的。

可以这样来理解,如果 if-else 是用来判断类型的,那么此时应该愉快的使用多态太替代,对于一般状态的判断则可以继续保留 if-else。诚然,在类型很多的时候,需要生成多个class, 这正是scala 提供的 case class 由于 java 的地方。

如何优化 if-else 或者 switch-case 作为类型判断使用的情况:

  1. 将每个条件创建一个对应的类型class

  2. 将每个分支中的业余逻辑放入对应类型的class中

这样就将类型判断交给多态来进行

TDD - 测试驱动开发

测试驱动开发步骤

test -> coding -> refactor (repeat) -> commit

从测试用用户的角度看代码,能够让自己更关注API的易用性

测试中构造虚拟依赖的方式

对于开发的程序功能中需要外部依赖的,例如依赖于HTTP,依赖于其他公司提供的接口,但是这些东西通常在开发环境中是不容易得到的。这个时候就要创建虚拟对象来替代,保证完成完整的测试。

那么构造虚拟对象的方法有: Stubbing 和 Mock Object。

Stubbing 的意思就是替换,将原本真正的对象通过接口,使用测试对象提供数据,也就是偷梁换柱

因此从这个方面考虑的话,将持久层DAO提供出接口就不仅仅是考虑和数据库接口了,而同时也是为了测试时候的方便。采用接口可以方便的构造测试对象,不必依赖具体的数据存储引擎。

采用Stubbing的方法主要是用来替代一些难以接触到的外部系统,例如Http Connection, File System, DataBase 等。不过要使用好Stubbing要求外部系统要提供良好的接口,这样可以通过测试模拟一个外部系统

Mock Object

Mock objects (or mocksfor short) are perfectly suitedfor testing a portion of code logic in isolation from the rest of the code. Mocks replace the objects with which your methods under test collaborate, offering a layer of isolation.

Mock 和 Stubbing确实是相似的,都是提供某种对真是对象的替代,但是Mock并不去实现替代对象的业务逻辑,而Stubbing却需要实现部分完成测试所需的逻辑。Mock只是一层空壳。

Mocks replace real objects from the inside, without the calling classes being aware of it

常见Mock手法:

  1. Mock 方法工厂

对于有些要Mock的类没有接口的时候,可以通过使用 方法工厂(method factory) 来进行。简而言之就是将需要测试给定的输入的地方提取为一个方法或者一个方法类,然后创建测试类继承目标类,重写之前提取出来的方法。

  1. Mock 类工厂

无论哪种方法,目的都是保证测试的时候让测试流程能够完整的执行。通过Mock填补外部系统的依赖,保证流程完整执行,就像拼图一样。