1. Junit 的介绍

JUnit 的使用非常容易上手,测试类放在 src/main/test;下,然后类名一般用 Test 结尾 ;

通常来说,一个测试类对应一个系统类,类中的每个方法,用于测试一个方法,方法一般以 test 开头,每个方法用 @Test 标注;

如果有测试开始前要执行的操作,用 @Before 标注一个方法来执行;如果有测试结束之后要执行的操作,用 @After 标注一个方法来执行,一般会将多个测试方法中的初始化和资源清理代码放在这些方法中;

每次运行一个方法,都会创建一个类的实例,将各个测试方法互相之间隔离开来,避免互相影响,导致测试失败;

在每个测试方法中,最后都会用 Assert 类来进行断言,判断测试是否通过,就是看一下测试得到的结果,是不是我们期望的,如果不是会报错,表示测试不通过;

JUnit 还有一个 Suite 的概念,一次性运行多个测试用例,算是一个测试套件,比如下面这样子:

  1. @RunWith(Suite.class)
  2. @SuiteClasses({TestSuite1.class, TestSuite2.class})
  3. public class TestSuitMain {
  4. }

我们一般都是,写好一些代码组件,然后就针对这些代码组件,去立即写对应的单元测试,单元测试是一个类一个类的执行

到了最后,你都要集成测试,mvn test,将所有的单元测试全部跑一遍;

2. spring-boot-starter-test

spring boot 对单元测试提供了很好的支持,只要依赖 spring-boot-starter-test 即可,这个依赖会自动导入单元测试需要的所有依赖,包括了 JUnit、AssertJ、Hamcrest 以及其他的一些包,而且这个 spring-boot-starter-test 一般是设置为 test scope 即可;

具体来说,spring-boot-starter-test 导入之后,会包含下面这些东西:

  • JUnit:最经典的单元测试框架;
  • Spring Test、Spring Boot Test:是 spring 和 spring boot 环境下,对测试的一个支持;
  • AssertJ:是用来进行断言的;
  • Hamcrest:是用来进行复杂断言,复杂的表达式;
  • Mockito:测试替身的模拟;
  • JSONassert:都是对 json 数据进行操作的;
  • JsonPath

    3. 测试 spring boot 应用程序

    3.1 springboot 单元测试脚手架

    ```java // @RunWith的意思,是不要使用默认方式进行单元测试,而是使用指定的类来提供单元测试 // 所有的spring测试都是找SpringRunner.class @RunWith(SpringRunner.class) // 这个是 spring boot 提供的,会一直找到一个 Application 类, // 只要包含了@SpringBootApplication 的就算,然后会先启动这个类,来给单元测试提供环境 @SpringBootTest public class EmployeeServiceTest {

    // 这里就可以从启动的 spring 上下文中,将EmployeeService注入到这里来,供我们进行测试 @Autowired private EmployeeService employeeService;

    @Test public void testFindById() {

    }

}

  1. <a name="EC4L2"></a>
  2. ### 3.2 测试 service 组件
  3. 要考虑的地方有3点:
  4. - 测试之前自己构造好数据,测试结束之后自动回滚数据构造;
  5. - 将 service 依赖的 dao 进行模拟打桩进来;
  6. - 可能需要在数据库中构造好数据;
  7. spring boot 会默认在单元测试结束之后,进行数据库事务回滚;<br />spring boot 会集成 mockito 框架来模拟依赖 dao 进行打桩;
  8. ```java
  9. /*
  10. * 这个注解是说,在执行单元测试的时候,不是直接去执行里面的单元测试的方法
  11. * 因为那些方法执行之前,是需要做一些准备工作的,它是需要先初始化一个spring容器的
  12. * 所以得找这个SpringRunner这个类,来先准备好spring容器,再执行各个测试方法
  13. */
  14. @RunWith(SpringRunner.class)
  15. /*
  16. * 这个是说,会从最顶层的包结构开始招,com.zhss.springboot
  17. * 找到一个标注了@SpringBootApplication注解的一个类,算是启动类
  18. * 然后会执行这个启动类的main方法,就可以创建spring容器,给后面的单元测试提供完整的这个环境
  19. */
  20. @SpringBootTest
  21. public class UserServiceImplTest {
  22. /**
  23. * 用户管理模块的service组件
  24. */
  25. @Autowired
  26. private UserService userService;
  27. /**
  28. * 这里加了@MockBean的注解
  29. * 就代表了说,这个UserDAO就不会用我们定义的那个userDAO了
  30. * 这里会由spring boot整合mockito框架,然后创建一个实现了UserDAO接口的匿名实现类
  31. * 然后将这个模拟出来实现了UserDAO接口的类的实例bean,放入spring容器中
  32. * 替代我们自己的那个UserDAO
  33. */
  34. @MockBean
  35. private UserDAO userDAO;
  36. /**
  37. * 测试用例:查询所有用户信息
  38. */
  39. @Test
  40. public void testListUsers() {
  41. // 准备好mock userDAO的返回数据
  42. List<User> users = new ArrayList<User>();
  43. User user = new User();
  44. user.setId(1L);
  45. user.setName("测试用户");
  46. user.setAge(20);
  47. users.add(user);
  48. // 对userDAO进行mock逻辑设置
  49. when(userDAO.listUsers()).thenReturn(users);
  50. // 测试UserSerivce的listUsers()方法
  51. List<User> resultUsers = userService.listUsers();
  52. // 对测试结果进行断言
  53. assertEquals(users, resultUsers);
  54. }
  55. /**
  56. * 测试用例:根据ID查询一个用户
  57. */
  58. @Test
  59. public void testGetUserById() {
  60. Long userId = 1L;
  61. User user = new User();
  62. user.setId(userId);
  63. user.setName("测试用户");
  64. user.setAge(20);
  65. when(userDAO.getUserById(userId)).thenReturn(user);
  66. User resultUser = userService.getUserById(userId);
  67. assertEquals(user, resultUser);
  68. }
  69. /**
  70. * 测试用例:新增用户
  71. */
  72. @Test
  73. public void testSaveUser() {
  74. Long userId = 1L;
  75. User user = new User();
  76. user.setName("测试用户");
  77. user.setAge(20);
  78. when(userDAO.saveUser(user)).thenReturn(userId);
  79. Long resultUserId = userService.saveUser(user);
  80. assertEquals(userId, resultUserId);
  81. }
  82. /**
  83. * 测试用例:修改用户
  84. */
  85. @Test
  86. public void testUpdateUser() {
  87. Long userId = 1L;
  88. User user = new User();
  89. user.setId(userId);
  90. user.setName("测试用户");
  91. user.setAge(20);
  92. when(userDAO.updateUser(user)).thenReturn(true);
  93. Boolean updateResult = userService.updateUser(user);
  94. assertTrue(updateResult);
  95. }
  96. /**
  97. * 测试用例:删除用户
  98. */
  99. @Test
  100. public void testRemoveUser() {
  101. Long userId = 1L;
  102. when(userDAO.removeUser(userId)).thenReturn(true);
  103. Boolean removeResult = userService.removeUser(userId);
  104. assertTrue(removeResult);
  105. }
  106. }

3.3 测试 dao 组件

/**
 * 用户管理模块的DAO组件的单元测试类
 * 
 * 单元测试尽量不要依赖外部,但是直到最后一层的时候,DAO层的时候,跟redis,rabbitmq打交道
 * 还是要依靠开发环境里的基础设施,来进行单元测试
 * 
 * @author zhonghuashishan
 *
 */
@RunWith(SpringRunner.class) 
@SpringBootTest
/*
 * @Transactional注解加了之后,就可以让每个方法都是放在一个事务里面
 * 接着是这样子,比如我们要执行一些数据库相关的操作,比如说测试mapper,是需要实际操作数据库的
 * 那么在测试查询操作的时候,我们需要通过程序预先灌入一些数据,再测试能否查询到这些数据
 * 接着测试完之后,其实应该让这个事务要回滚,就可以自动取消我们插入的那些数据了
 * 让单元测试方法执行的这些增删改的操作,都是一次性的
 */
@Transactional 
@Rollback(true)
public class UserDAOImplTest {

    /**
     * 用户管理模块的DAO组件
     */
    @Autowired
    private UserDAO userDAO;

    /**
     * 测试用例:查询所有用户信息
     */
    @Test
    public void testListUsers() {
        // 准备好mock userMapper的返回数据
        User user = new User();
        user.setName("测试用户");  
        user.setAge(20);
        userDAO.saveUser(user);

        List<User> users = new ArrayList<User>();
        users.add(user);

        // 测试UserSerivce的listUsers()方法
        List<User> resultUsers = userDAO.listUsers();

        // 对测试结果进行断言
        assertEquals(users.size(), resultUsers.size());  
    }

    /**
     * 测试用例:根据ID查询一个用户
     */
    @Test
    public void testGetUserById() {
        User user = new User();
        user.setName("测试用户");  
        user.setAge(20);
        userDAO.saveUser(user);

        Long userId = user.getId();

        User resultUser = userDAO.getUserById(userId);

        assertEquals(user.toString(), resultUser.toString());  
    }

    /**
     * 测试用例:新增用户
     */
    @Test
    public void testSaveUser() {
        User user = new User();
        user.setName("测试用户");  
        user.setAge(20);

        Long resultUserId = userDAO.saveUser(user);

        assertThat(resultUserId, is(greaterThan(0L)));
    }

    /**
     * 测试用例:修改用户
     */
    @Test
    public void testUpdateUser() {
        Integer oldAge = 20;
        Integer newAge = 21;

        User user = new User();
        user.setName("测试用户");  
        user.setAge(oldAge);
        userDAO.saveUser(user);

        user.setAge(newAge); 
        Boolean updateResult = userDAO.updateUser(user);

        assertTrue(updateResult); 

        User updatedUser = userDAO.getUserById(user.getId());

        assertEquals(newAge, updatedUser.getAge());
    }

    /**
     * 测试用例:删除用户
     */
    @Test
    public void testRemoveUser() {
        User user = new User();
        user.setName("测试用户");  
        user.setAge(20);
        userDAO.saveUser(user);

        Boolean removeResult = userDAO.removeUser(user.getId());

        assertTrue(removeResult);
    }

}

spring boot 有一个 @Sql 注解,可以在测试开始前执行 SQL 语句初始化数据;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
@Rollback(true)
public class EmployeeMapperTest {

    @Autowired
    private EmployeeMapper employeeMapper;

    @Test
    // 这个@Sql注解,就会在我们执行测试之前,先执行sql语句,初始化数据
    @Sql({"employee.sql"})
    public void testAddEmployee() {
        Employee employee = new Employee();
        employee.setName("李四");
        employee.setAge(30);

        Long employeeId = employeeService.add(employee);
        employee.setId(employeeId);
        assertTrue(employeeId > 0);

        // 接着需要从数据库中查询数据来比较
        Employee resultEmployee = employeeMapper.findById(1);
        assertEquals(employee, resultEmployee);
    }

}

3.4 测试 Controller 组件

/**
 * 用户管理模块的控制器组件的单元测试类
 * @author zhonghuashishan
 *
 */
@RunWith(SpringRunner.class)
/*
 * 通过这个注解表明,你要测试的controller是谁
 */
@WebMvcTest(UserController.class)  
public class UserControllerTest {

    /**
     * 注入一个MockMvc,模拟对controller发起http请求
     */
    @Autowired
    private MockMvc mockMvc;
    /**
     * 模拟userService组件
     */
    @MockBean
    private UserService userService;

    /**
     * 测试用例:查询所有用户信息
     */
    @Test
    public void testListUsers() {
        try {
            List<User> users = new ArrayList<User>();

            User user = new User();
            user.setId(1L);
            user.setName("测试用户");  
            user.setAge(20);

            users.add(user);

            when(userService.listUsers()).thenReturn(users);

            mockMvc.perform(get("/api/v1.0/user/"))
                    .andExpect(content().json(JSONArray.toJSONString(users))); 
        } catch (Exception e) {
            e.printStackTrace(); 
        }
    }

    /**
     * 测试用例:根据ID查询一个用户
     */
    @Test
    public void testGetUserById() {
        try {
            Long userId = 1L;

            User user = new User();
            user.setId(userId);
            user.setName("测试用户");  
            user.setAge(20);

            when(userService.getUserById(userId)).thenReturn(user);

            mockMvc.perform(get("/api/v1.0/user/{id}", userId))  
                    .andExpect(content().json(JSONObject.toJSONString(user)));  
        } catch (Exception e) {
            e.printStackTrace(); 
        }
    }

    /**
     * 测试用例:新增用户
     */
    @Test
    public void testSaveUser() {
        Long userId = 1L;

        User user = new User();
        user.setName("测试用户");  
        user.setAge(20);

        when(userService.saveUser(user)).thenReturn(userId);

        try {
            mockMvc.perform(post("/api/v1.0/user/").contentType("application/json").content(JSONObject.toJSONString(user)))  
                    .andExpect(content().json("{'status': 'success', 'message': '新增用户ID为" + user.getId() + "'}"));   
        } catch (Exception e) {
            e.printStackTrace(); 
        }
    }

    /**
     * 测试用例:修改用户
     */
    @Test
    public void testUpdateUser() {
        Long userId = 1L;

        User user = new User();
        user.setId(userId); 
        user.setName("测试用户");  
        user.setAge(20);

        when(userService.updateUser(user)).thenReturn(true);

        try {
            mockMvc.perform(put("/api/v1.0/user/{id}", userId).contentType("application/json").content(JSONObject.toJSONString(user)))  
                    .andExpect(content().string("success"));     
        } catch (Exception e) {
            e.printStackTrace(); 
        }
    }

    /**
     * 测试用例:删除用户
     */
    @Test
    public void testRemoveUser() {
        Long userId = 1L;

        when(userService.removeUser(userId)).thenReturn(true);  

        try {
            mockMvc.perform(delete("/api/v1.0/user/{id}", userId))   
                    .andExpect(content().string("success"));     
        } catch (Exception e) {
            e.printStackTrace(); 
        }
    }

}

3.5 mvc 请求模拟

模拟 GET 请求:mvc.perform(get(“/employee/{id}”, 1))
模拟 POST 请求:mvc.perform(post(“/employee/{id}”, 1))
模拟文件上传:mvc.perform(multipart(“/upload”).file(“file”, “文件内容”.getBytes(“UTF-8”)))
模拟表单请求:mvc.perform(post(“/employee”).param(“name”, “张三”).param(“age”, 20))
模拟 session:mvc.perform(get(“/employee”).sessionAttr(name, value))
模拟 cookiei:mvc.perform(get(“/employee”).cookie(new Cookie(name ,value)))
模拟 HTTP body 内容,比如 json:mvc.perform(get(“/employee”).content(json))

模拟设置 HTTP header:
mvc.perform(get(“/employee/{id}”), employeeId)
.contentType(“application/x-www-form-urlencoded”)
.accept(“application/json”)
.header(header, value)

3.6 比较 mvc 请求返回结果

mvc.perform(get("/employee"))
        .andExpect(status().isOk()) // 响应是否为200状态码
        .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 返回的content type是不是application/json
        .andExpect(jsonPath("$.name").value("张三")) // 返回内容本身检查

mvc.perform(post("/employee"))
        .andExpect(model().attribute("name", "张三"))
        .andExpect(model().attributeExists("name"))

4. Mockito 使用

假设我们有一个接口,需要来模拟这个接口的实现类对象

public interface EmployeeService {

    public Employee findById(Long id);
    public boolean add(Employee employee);

}
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class EmployeeServiceTest {

    @Test
    public void test() {
        // 模拟出来一个实现了EmployeeService接口的对象
        EmployeeService employeeService = mock(EmployeeService.class);
        // 这个对象的findById()方法无论传入什么参数,都是返回预定义的一个对象
        when(employeeService.findById(anyLong())).thenReturn(employee);
        // 尝试调用
        Employee resultEmployee = employeeServicie.findById(1);
        // 比较结果
        assertEquals(employee, resultEmployee);
    }

}

还可以检查一个接口被调用的次数:

when(employeeService.findById(eq(1))).thenReturn(employee);
Employee resultEmployee = employeeService.findById(1);
assertEquals(employee, resultEmployee);

// 这里就是检查employeeService的findById(1)调用是否为2次
verify(employeeService ,times(2)).findById(eq(1));

此外,还可以检查多个接口被调用的顺序:

EmployeeService employeeService = mock(EmployeeService.class);
when(employeeService.findById(eq(1))).thenReturn(employee);
when(employeeService.add(eq(employee))).thenResult(true);
boolean addResult = employeeService.add(employee);
Employee resultEmployee = employeeService.findById(1);

// 这里就是在验证,是否按照下面的顺序完成的调用
InOrder inOrder = inOrder(employeeService);
inOrder.verify(employeeService).add(employee);
inOrder.verify(employeeService).findById(1);

还可以模拟抛出异常:

EmployeeService employeeService = mock(EmployeeService.class);
when(employeeService.findById(lt(0))).thenThrow(new IllegalArgumentException("employeeId不能小于0"))

Employee reseultEmployee = employeeService.findById(-1);