JUnit与Mockito单元测试典型示例

单元测试的重要性无需赘述,但单元测试也会遇到困难,其中之一就是如何创建依赖。试想我们常见的server端分层架构,数据访问层Dao,业务层,以及Web层,想要单元测试业务层,我们需要依赖Dao层提供数据支持,Dao层又依赖数据库,数据库需要shema以及data。为了测试业务逻辑,竟然需要准备那么多东西,想想就麻烦,单元测试的热情也减去大半。这个问题的解法就是mock技术,模拟被依赖的组件的行为

先聊聊单元测试

单元测试简单来说就是类的测试,测试对象是某一个类,测试内容是类逻辑的正确性,这里强调一下,测试内容只关注被测试类自身的逻辑,被测试类所依赖类的逻辑,理应由其它单元测试去cover。下图是一个简单的示意图:

JUnit与Mockito单元测试典型示例_第1张图片

单元测试只关注Target的逻辑。如果没有Target Dependency,那么当然更好,直接测试。万一有Target Dependency呢?有些时候构建Target Dependency很简单,new一个对象即可。如果向开头说的那种Dao依赖类呢?硬着头皮创建数据库,初始化表结构和数据,当然可以,但是这些与我们要测试的业务层的正确性没啥关系。

一个更经济环保的做法是mock Dao类。可是如何mock?mock什么内容呢?Java世界里面有很多优秀的mock工具,比如EasyMock、jMock以及Mockito,可以用来简化我们的mock工作。本文只讨论mockito。至于mock什么内容的问题,我的理解是mock类的行为,也就是类的Method,Value Object直接创建即可。类的Method大概分为以下几个内容:

  • Method名字
  • Method输入参数
  • Method输出返回值
  • Method异常
  • Method执行语句

Method名字当然无法改变,没有mock的说法。其它的都可以mock。

Dao mock示例

先准备entity以及Dao接口:

public class User {
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

public interface UserDao {
    /**
     * 新增用戶
     */
    void insertUser(User user);

    /**
     * 查詢用戶
     */
    User queryUser(Long id);
}

下面是业务服务以及实现,TDD的做法是先UnitTest后服务实现,关注的不
是TDD,而是mock,这里暂时先不较真。

public interface UserService {
        /**
         * 创建新用戶
         */
        void createNewUser(User user) throws Exception ;
    }

    public class UserServiceImpl implements UserService {
    private UserDao userDao;

    public void createNewUser(User user) throws Exception {
        // 参数校验
        if (user == null || user.getId() == null || isEmpty(user.getName())) {
            throw new IllegalArgumentException();
        }

        // 查看是否是重复数据
        Long id = user.getId();
        User dbUser = userDao.queryUser(id);
        if (dbUser != null) {
            throw new Exception("用户已经存在");
        }

        try {
            userDao.insertUser(dbUser);
        } catch (Exception e) {
            // 隐藏Database异常,抛出服务异常
            throw new Exception("数据库语句执行失败", e);
        }
    }

    private boolean isEmpty(String str) {
        if (str == null || str.trim().length() == 0) {
            return true;
        }
        return false;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

开始单元测试:

import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import static org.mockito.Mockito.*;

import java.sql.SQLException;

public class UserServiceTest {

    @Test(expected = IllegalArgumentException.class)
    public void testNullUser() throws Exception {
        UserService userService = new UserServiceImpl();

        // 创建mock
        UserDao userDao = mock(UserDao.class);
        ((UserServiceImpl) userService).setUserDao(userDao);

        userService.createNewUser(null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testNullUserId() throws Exception {
        UserService userService = new UserServiceImpl();

        // 创建mock
        UserDao userDao = mock(UserDao.class);
        ((UserServiceImpl) userService).setUserDao(userDao);

        User user = new User();
        user.setId(null);
        userService.createNewUser(user);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testNullUserName() throws Exception {
        UserService userService = new UserServiceImpl();

        // 创建mock
        UserDao userDao = mock(UserDao.class);
        ((UserServiceImpl) userService).setUserDao(userDao);

        User user = new User();
        user.setId(1L);
        user.setName("");
        userService.createNewUser(user);
    }

    @Test(expected = Exception.class)
    public void testCreateExistUser() throws Exception {
        UserService userService = new UserServiceImpl();

        // 创建mock
        UserDao userDao = mock(UserDao.class);
        User returnUser = new User();
        returnUser.setId(1L);
        returnUser.setName("Vikey");
        when(userDao.queryUser(1L)).thenReturn(returnUser);
        ((UserServiceImpl) userService).setUserDao(userDao);

        User user = new User();
        user.setId(1L);
        user.setName("Vikey");
        userService.createNewUser(user);
    }

    @Test(expected = Exception.class)
    public void testCreateUserOnDatabaseException() throws Exception {
        UserService userService = new UserServiceImpl();

        // 创建mock
        UserDao userDao = mock(UserDao.class);
        doThrow(new SQLException("SQL is not valid")).when(userDao).insertUser(any(User.class));
        ((UserServiceImpl) userService).setUserDao(userDao);

        User user = new User();
        user.setId(1L);
        user.setName("Vikey");
        userService.createNewUser(user);
    }

    @Test
    public void testCreateUser() throws Exception {
        UserService userService = new UserServiceImpl();

        // 创建mock
        UserDao userDao = mock(UserDao.class);
        doAnswer(new Answer() {
            public Void answer(InvocationOnMock invocation) throws Throwable {
                System.out.println("Insert data into user table");
                return null;
            }
        }).when(userDao).insertUser(any(User.class));
        ((UserServiceImpl) userService).setUserDao(userDao);

        User user = new User();
        user.setId(1L);
        user.setName("Vikey");
        userService.createNewUser(user);
    }
}

前三个测试案例,测试的是服务对非法输入参数的处理,用不到UserDao,无需mock。第4个测试案例测试的是服务的处理逻辑,UserService如何处理新增已经存在用户的问题,我们期望报错。判断用户是否存在,需要用到UserDao.queryUser接口,所以我们mock queryUser方法。当参数是1L时,返回一个User对象。第5个测试案例测试Dao抛异常的情况.第6个测试案例测试正常处理的情形,doAnswer只是展示无返回值Method的用法,可以不写。

结语

Mockito的功能很强大,具有记忆Method调用的功能,不过我觉得UnitTest不应该过多关注mock,而是Target。Mockito的语法刚接触会觉得很奇怪,和我们平常接触的Java代码对不上,熟悉了之后会觉得它的API非常精美。

参考链接

  • Mockito作者解释背后的设计思想
  • 反模式的经典 - Mockito设计解析

你可能感兴趣的:(Core,Java)