单元测试并不是一门很复杂的技术,我相信很多程序员在刚开始工作的时候也都对单元测试有了基本的掌握。但是,近我在实际工作中发现,很多时候单元测试并没有发挥其应有的作用,更多的时候成了一种提高代码测试覆盖率的手段。下面我谈谈我对单元测试的看法以及我的一些经验。

  单元测试的意义

  这是一个很多人都知道答案的问题,但我还是要多唠叨几句。单元测试不仅仅是一种测试的手段,它更是一种设计方式,是一种保障。在 TDD 中,单元测试可以避免过度设计。而在代码重构中,单元测试同其它自动化测试一道,保障的重构可以顺利进行。

  单元测试测什么

  简单来说,单元测试测试的是被测试单元(通常是一个方法)所承诺的功能。单元测试通常是白盒测试(后面会提到例外的情况),单元测试不应关注被测方法的内部实现,而应该检查方法的返回值、方法对变量状态的改变、方法所做的重要动作(例如发送消息的内容、写文件的内容、数据库操作的结果,等等)。

  单元测试的范围

  一个项目中不是所有的代码都需要单元测试覆盖,执意对代码覆盖率高追求是不正确的。但这引出一个问题,哪些代码更需要单元测试,哪些代码则不是很需要。单元测试的一个重要意义是保证代码的变动不会破坏其应有的功能,所以,变动的可能相对较小的代码,其代码需要单元测试的意义也相对较小。例如,如果你的 DAL (Data Access Layer) 采用了 Hibernate,那增删查改的这些方法边不需要单元测试的。因为这些代码基本上是对一个框架 API 的封装而已。

  单元测试的形式

  一个单元测试大致可以分成三部分,其实这也是很多测试的形式,即 Given-When-Then。首先是给出前置条件,例如这个方法的入参是多少、这个方法所属实例的变量的状态、相关环境(文件、数据库)等的状态。然后是调用被测试的方法。后是对结果的检查。

  在想 Spock 等的 BDD 测试框架中,一个测试用例的形式如下:

  Java代码


import spock.lang.Specification;

class RomanCalculatorSpec extends Specification {
    def "I plus I should equal II"() {
        given:
            def calculator = new RomanCalculator()
        when:
            def result = calculator.add("I", "I")
        then:
            result == "II"
    }
}
 


  如果是使用 JUnit 或者 TestNG 这样的单元测试框架,我也建议用 Given-When-Then 的形式划分测试代码,这样代码的意图显得很清晰。

  单元测试 与 Mock

  单元测试主要是靠 xUnit 类的框架实现的,但是只用 xUnit 在很多情况下不能实现单元测试。因为在实际工作中,被测试的代码很多都是有环境依赖的。而这种依赖,在单元测试中通常是无法提供的。所以需要用 Mock 框架来消除这种依赖。这是 Mock 框架所提供的主要功能之一。Mock 框架的另一个重要功能是它可以验证其所 Mock 的类或接口中的某个方法在测试过程中是否被调用。虽然这种做法不符合白盒测试的原则,但当你因为在单元测试中无法实现而 Mock 某个方法时,而这个方法又是十分关键的,通过 Mock 框架来检查这个方法是否按期望被调用也是可以的。例如,你的 UT 所测试的功能需要调用一个数据库相关的操作,这个操作十分重要,是你 UT 被测方法的所承诺的关键功能,但是这个操作偏偏不能在 UT 中直接执行,或者这么做起来很费劲。这时可以用 Mock 框架来上场了。下面以 Mockito 为例简单说说 Mock 的形式。

  Java代码


List mockList = mock(List.class);
mockList.add("one");

verify(mockList).add("one");
 


  (上面这段在 Mockito 项目主页上也有,比较简单,不解释)