欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

单元测试那些事儿 博客分类: 经验总结 单元测试java 

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

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

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

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

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

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

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 的形式。

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

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

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

如果想要检查被 Mock 的方法的入参是什么?这往往是很重要的。否则,入参不正确,怎么保证 Mock 的方法被正确调用了呢?

假设,你要测试的方法依赖 UserDAO,因为 UserDAO 依赖数据库环境(虽然现在有基于内存存储和基于文件存储的轻便关系型数据库,使得在一些场景下,基于数据库的方法可以在 UT 中直接被测试,但在一些情况下,例如基于某种数据库特殊语法的操作,存储过程的操作等等,这些还是很难直接在 UT 中被测试)

UserDAO:
Collection users = query(UserCriteria criteria);


你可会这样写 Mock

UserDao userDao = mock(UserDAO.class);
when(userDao.query(new UserCriteria())).thenReturn(users);// users 是你设定好的返回值


不幸的是,这样写是错的。即便是这样写

when(userDao.query(any(UserCriteria.class))).thenReturn(users);// users 是你设定好的返回值


这样写虽然可以正常运行,但是这样的测试是不可靠。因为不论 UserCriteria 里的参数是什么,你所 Mock 出来的方法返回的都是你期望的值。这显然是与现实不符的。这是就需要 Hamcrest 的 Matcher 登场了

Matcher matcher = new BaseMatcher() {
    @Override
    public boolean matches(Object o) {
        UserCriteria criteria = (UserCriteria) o;
        assert criteria.field1;
        assert criteria.field2;
        assert criteria.field3;
        ....
        return true;
    }
};

when(userDao.query(argThat(matcher))).thenReturn(users);// users


注意,这里的 assert 指的是 JUnit 的 assertEquals 等,或 assertThat (我更推荐)。当然,TestNG 的也是可以。但不要使用 Java 中的 assert 关键字。

这样,你就可以对你 Mock 的方法的入参做细致的检查了。

测试支持代码
一个项目的单元测试的代码通常都是直接针对项目主目录中代码的测试,但是一些情况下,你需要针对你所使用的技术开发一些代码以辅助你的单元测试。这些代码既不是你的项目功能实现,也没有测试任何一个方法。一个优秀的框架,尤其是那些非基础设施类的框架(基础设施类的框架如数据库相关的 Hibernate、iBatis 等),往往都提供了方便用户单元测试的辅助代码。例如,Spring Framework 有专门用户测试的子模块 spring-test,Spring MVC 也有帮助测试的类。Apache Camel 也有专门用于简化测试的子项目(Camel Test)。但如果不凑巧,你用的技术直接测试起来不方便,同时有没有相关的简化测试的类库,那就不要犹豫了,自己丰衣足食吧。(我在项目中用到的 Sip Servlet 就是这么一个非主流的技术)
相关标签: 单元测试 java