Spring MVC 单元测试学习

Spring MVC 单元测试学习

1.前言

这次来介绍下传统Spring MVC中对单元测试的整合使用,本篇会通过以下3点来介绍,基本满足日常需求:

  • Dao层单元测试
  • Service层单元测试
  • Controller层单元测试

在单元测试中要尽量使用断言,本文所有的测试类都符合几个原则:

  • 测试类卸载src/test/java目录下
  • 测试类的包结构与被测试类的包结构相同
  • 测试类的命名都是被测试类类名后缀加上Test,例如,UserDaoImpl与UserDaoImplTest相对应
  • 测试类的方法与被测试类的方法命名相同

2.正文

2.1核心依赖

在Spring MVC 项目中引入单元测试很简单,依赖如下:

<!-- 测试框架 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

2.2 如何创建单元测试类

Spring Boot中单元测试类写在在src/test/java目录下,你可以手动创建具体测试类,如果是IDEA,则可以通过IDEA自动创建测试类,如下图,可以通过快捷键Ctrl+Shift+T(Window)来创建,如下:

2.3. 盲点解释

@Runwith

JUnit用例都是在Runner(运行器)来执行的。通过它,可以为这个测试类指定一个特定的Runner。
JUnit允许用户指定其它的单元测试执行类,只需要我们的测试执行类继承类org.junit.runners.BlockJUnit4ClassRunner就可以了,Spring的执行类SpringJUnit4ClassRunner就是继承了该类。我们平时用Spring也比较多,为了能够更加方便的引用配置文件,我们单元测试就使用了Spring实现的执行类。此时的单元测试执行类将会看起来是这样:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-context.xml")
public class UserServiceImplTest {

    @Autowired
    private UserService service;

    /**
     * 测试根据用户名查询用户
     */
    @Test
    public void getByUsername() {
        User user = service.getByUsername("小明");
        System.err.println(user);
    }

SpringJUnit4ClassRunner

SpringJUnit4ClassRunner是JUnit的BlockJUnit4ClassRunner类的一个常规扩展,提供了一些spring测试环境上下文去规范JUnit测试。

@ContextConfiguration

注解指定了一个测试类运行了Spring 容器环境。

@WebAppConfiguration

测试环境使用,用来表示测试环境使用的ApplicationContext将是WebApplicationContext类型的;value可以指定web应用的根;
Spring MVC测试步骤
直接在测试类上面加上如下2个注解

  • @RunWith(SpringJUnit4ClassRunner.class)
  • @ContextConfiguration(locations = “classpath:spring-context.xml”)

就能取到spring中的容器的实例,如果配置了@Autowired那么就自动将对象注入。

注意:

如果要对Controller层测试,那么在你的Spring容器中要加入MVC容器的配置。

单元测试回滚

单元个测试的时候如果不想造成垃圾数据,可以开启事物功能,记在方法或者类头部添加@Transactional注解即可,如下:

@Transactional
@Test
public void save() {
    User user = new User("测试用户", "123456");
    int save = dao.save(user);
    Assert.assertEquals(1, save);
}

这样测试完数据就会回滚了,不会造成垃圾数据。

3.核心代码示例

3.1.Spring容器配置

spring-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <context:annotation-config/>
    <context:component-scan base-package="com.littlefxc.examples.service"/>
    <context:component-scan base-package="com.littlefxc.examples.dao"/>

    <context:property-placeholder location="classpath:druid.properties"/>

    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close">
        <property name="url" value="${spring.datasource.druid.url}"/>
        <property name="username" value="${spring.datasource.druid.username}"/>
        <property name="password" value="${spring.datasource.druid.password}"/>
        <property name="driverClassName" value="${spring.datasource.druid.driver-class-name}"/>
    </bean>

    <bean class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 注解式事务配置 -->
    <bean id="txManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <tx:annotation-driven transaction-manager="txManager"/>

    <import resource="spring-mvc.xml" />
</beans>

spring-mvc.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="
       http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven />

    <context:component-scan base-package="com.littlefxc.examples.controller"/>
</beans>

3.2.Dao层单元测试

基本上所有的WEB程序都会涉及到数据库,本次示例就以最简化的模式:
持久层框架就用spring-jdbc。
Dao层的测试涉及到基本的CRUD操作。

UserDaoImpl

@Repository
public class UserDaoImpl implements UserDao {

    @Autowired
    private JdbcTemplate template;

    private static final BeanPropertyRowMapper<User> MAPPER = new BeanPropertyRowMapper<>(User.class);

    /**
     * 新建用户
     *
     * @param user
     * @return
     */
    @Override
    public int save(User user) {
        return template.update(
                "insert into user(username, password) values (?, ?)",
                user.getUsername(), user.getPassword());
    }

    /**
     * 修改密码
     *
     * @return
     */
    @Override
    public int update(User user) {
        return template.update(
                "update user set password = ? where username = ?", user.getPassword(), user.getUsername());
    }

    /**
     * 根据ID删除用户
     *
     * @return
     */
    @Override
    public int delete(Long id) {
        return template.update("delete from user where id = ?", id);
    }

    /**
     * 获取所有用户
     *
     * @return
     */
    @Override
    public List<User> list() {
        return template.query("select id, username, password from user", MAPPER);
    }

    @Override
    public User getByUsername(String username) {
        try {
            return template.queryForObject(
                    "select id, username, password from user where username = ? limit 1", MAPPER, username);
        } catch (EmptyResultDataAccessException e) {
            return null;
        }
    }
}

UserDaoImplTest

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-context.xml")
public class UserDaoImplTest {

    @Autowired
    private UserDao dao;

    /**
     * 测试根据用户名查询用户
     */
    @Test
    public void getByUsername() {
        User user = dao.getByUsername("小明");
        System.err.println(user);
    }

    /**
     * 测试新建用户
     */
    @Transactional
    @Test
    public void save() {
        User user = new User("测试用户", "123456");
        int save = dao.save(user);
        Assert.assertEquals(1, save);
    }

    /**
     * 测试修改密码
     */
    @Transactional
    @Test
    public void update() {
        User user = new User("测试用户", "123456");
        dao.save(user);
        /* 密码修改前 */
        User before = dao.getByUsername("测试用户");
        user.setPassword("654321");
        /* 密码修改后 */
        dao.update(user);
        User after = dao.getByUsername("测试用户");
        /* 断言判断修改密码前后的两个类不同 */
        Assert.assertNotEquals(before, after);
    }

    /**
     * 测试删除用户
     */
    @Transactional
    @Test
    public void delete() {
        int save = dao.save(new User("测试用户", "123456"));
        Assert.assertEquals(1, save);
        User user = dao.getByUsername("测试用户");
        int delete = dao.delete(user.getId());
        Assert.assertEquals(1, delete);
    }

    /**
     * 测试用户列表
     */
    @Test
    public void list() {
        List<User> list = dao.list();
        System.err.println(list);
    }
}

3.3.Service层单元测试

UserServiceImpl

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserDao dao;

    /**
     * 根据用户名查询用户
     *
     * @param username
     * @return
     */
    @Override
    public User getByUsername(String username) {
        return dao.getByUsername(username);
    }

    /**
     * 列表查询用户
     *
     * @return
     */
    @Override
    public List<User> list() {
        return dao.list();
    }

    /**
     * 修改密码
     * @return
     */
    @Transactional
    @Override
    public int updatePassword(User user) {
        return dao.update(user);
    }

    /**
     * 根据ID删除用户
     * @return
     */
    @Transactional
    @Override
    public int deleteById(Long id) {
        return dao.delete(id);
    }

    /**
     * 添加用户
     * @param user
     * @return
     */
    @Transactional
    @Override
    public int save(User user) {
        return dao.save(user);
    }
}

UserServiceImplTest

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-context.xml")
public class UserServiceImplTest {

    @Autowired
    private UserService service;

    /**
     * 测试根据用户名查询用户
     */
    @Test
    public void getByUsername() {
        User user = service.getByUsername("小明");
        System.err.println(user);
    }

    /**
     * 测试列表查询用户
     */
    @Test
    public void list() {
        List<User> list = service.list();
        System.err.println(list);
    }

    /**
     * 测试修改密码
     */
    @Transactional
    @Test
    public void updatePassword() {
        User user = new User("测试用户", "123456");
        service.save(user);
        /* 密码修改前 */
        User before = service.getByUsername("测试用户");
        user.setPassword("654321");
        /* 密码修改后 */
        service.updatePassword(user);
        User after = service.getByUsername("测试用户");
        /* 断言判断修改密码前后的两个类不同 */
        Assert.assertNotEquals(before, after);
    }

    /**
     * 测试根据ID删除用户
     */
    @Transactional
    @Test
    public void deleteById() {
        int save = service.save(new User("测试用户", "123456"));
        Assert.assertEquals(1, save);
        User user = service.getByUsername("测试用户");
        int delete = service.deleteById(user.getId());
        Assert.assertEquals(1, delete);
    }

    /**
     * 测试添加用户
     */
    @Transactional
    @Test
    public void save() {
        List<User> list = service.list();
        System.err.println(list);
    }
}

3.4.Controller层单元测试

上面只是针对Service和Dao层做测试,但是有时候需要对Controller层(API)做测试,这时候就得用到MockMvc了,你可以不必启动工程就能测试这些接口。

MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。

UserController

@RestController
public class UserController {

    @Autowired
    private UserService service;

    /**
     * 用户列表
     * @return
     */
    @RequestMapping("/user/list")
    public List<User> list() {
        return service.list();
    }

    /**
     * 新建用户
     * @param username
     * @param password
     * @return
     */
    @RequestMapping("/user/save")
    public int save(@RequestParam String username, @RequestParam String password) {
        return service.save(new User(username, password));
    }

    /**
     * 根据ID删除用户
     * @param id
     * @return
     */
    @RequestMapping("/user/delete")
    public int delete(@RequestParam Long id) {
        return service.deleteById(id);
    }

    /**
     * 根据用户名查询用户
     * @param username
     * @return
     */
    @RequestMapping("/user/getByUsername")
    public User getByUsername(@RequestParam String username) {
        return service.getByUsername(username);
    }

    /**
     * 根据用户名修改密码
     * @param username
     * @param newPassword
     * @return
     */
    @RequestMapping("/user/updatepassword")
    public int updatepassword(@RequestParam String username, @RequestParam String newPassword) {
        return service.updatePassword(new User(username, newPassword));
    }

}

UserControllerTest

/**
 * 1.@WebAppConfiguration:测试环境使用,用来表示测试环境使用的ApplicationContext将是WebApplicationContext类型的;
 * 2.@ContextConfiguration: 指定测试类的容器环境
 */
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {"classpath:spring-context.xml"})
public class UserControllerTest {

    /**
     * 模拟浏览器
     */
    MockMvc mvc;

    /**
     * 加载WEB的上下文
     */
    @Autowired
    WebApplicationContext context;

    @Before
    public void before() {
        //       mvc = MockMvcBuilders.standaloneSetup(new TestController()).build();
        mvc = MockMvcBuilders.webAppContextSetup(context).build();//建议使用这种
    }

    /**
     * 测试用户列表
     *
     * @throws Exception
     */
    @Test
    public void list() throws Exception {
        mvc.perform(
                MockMvcRequestBuilders.get("/user/list"))
                .andExpect(status().isOk()) // 期待返回状态码200
                .andDo(print()); // 打印返回的 http response 信息
    }

    /**
     * 测试添加用户
     * 期待:Body = 1
     *
     * @throws Exception
     */
    @Transactional
    @Test
    public void save() throws Exception {
        mvc.perform(
                MockMvcRequestBuilders.post("/user/save")
                        .param("username", "测试用户1")
                        .param("password", "123456"))
                .andExpect(status().isOk())
                .andDo(print());
    }

    /**
     * 测试删除用户
     * 期待:Body = 1
     */
    @Transactional
    @Test
    public void delete() throws Exception {
        mvc.perform(MockMvcRequestBuilders.post("/user/delete")
                .param("id", "1"))
                .andExpect(status().isOk())
                .andDo(print());
    }

    /**
     * 测试根据用户名查询用户
     */
    @Test
    public void getByUsername() throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/user/getByUsername")
                .param("username", "小明"))
                .andExpect(status().isOk())
                .andDo(print());
    }

    /**
     * 测试修改密码
     * 期待:Body = 1
     */
    @Transactional
    @Test
    public void updatepassword() throws Exception {
        mvc.perform(MockMvcRequestBuilders.post("/user/updatepassword")
                .param("username", "小明")
                .param("newPassword", "654321"))
                .andExpect(status().isOk())
                .andDo(print());
    }
}
  • 6
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值