# 简介

古语说: "做程序猿的男人, 都是好男人, 因为他每天都会问自己 '我 TM 到底错哪儿了' ".

今天, 我们就在一定程度上解决一下这个问题, 那就是使用单元测试的方式, 同单元测试, 让自己的每一小段代码都是可靠的, 避免了背锅. 即便是真的出现了问题, 也能帮助我们快速定位问题点.

# 创建项目

话不多说, 先上代码, 此项目是在 "第一个 SpringBoot 项目 HelloWorld" 基础上写的. 不过, 没有上一篇的基础影响也不大.

# HelloWorld 测试

步骤如下:

  • 添加测试相关的 jar
  • 创建业务代码
  • 创建测试代码
  • 执行测试

# 添加测试相关的 jar

pom.xml 文件中添加

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

# 创建业务代码

在启动主类 TestHelloApplication.java 同级目录下创建类 UserService.java 代码如下

/**
 * @author summer
 * @date 2018/4/4 下午5:21
 * 用户的 Service 服务
 */
@Service
public class UserService {
    /**
     * @author summer
     * @date 2018/4/4 下午5:35
     * @return java.lang.String
     * @description SpringBoot Junit 测试的 HelloWorld
     */
    public String hello(){
        System.out.println("Hello World");
        return "success";
    }
}

# 创建测试代码

src/test/java/net/abcbook/learn/springboot 下创建类 UserServiceTest.java 代码如下

PS: src/test 是 SpringBoot 默认的测试类的根目录, 一般其下的包名和 src/main/java 下的包名一致

/**
 * @author summer
 * @date 2018/4/4 下午5:23
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {
    /**
     * @author summer
     * @date 2018/4/4 下午5:37
     * @return void
     * @description SpringBoot + Junit 测试的 HelloWorld
     */
    @Test
    public void hello() throws Exception {
        String result = userService.hello();
    }
}

# 执行测试

在测试方法 hello 上右击, 点击运行, 我们又可以看到 Hello World 了. 是不是相当简单?

# 代码解释

我们已经正常运行了一个测试了, 那我们都做了说那些事情呢?

首先, 添加相关的 jar 文件, 这是 SpringBoot 的常用套路, 第一步一般都是添加 jar 包. 这里我们使用的 spring-boot-starter-test 一般会在创建项目的时候自动添加, 不需要我们自己添加. <scope>test</scope> 表示只在测试环境加载此中相关的 jar

UserService.java 这个就不解释了吧.

UserServiceTest.java

@RunWith 其实就是一个运行器, @RunWith(SpringRunner.class) 表示用 SpringRunner 运行

@SpringBootTestSpringBoot 项目的测试类的标注, 目的就是告诉 Spring 这是一个测试类

@Test 用过 junit 的同学应该对这个不陌生了, 没错, 就是标明当前方法为测试方法.

# Assert 断言

# 编写业务代码

UserService.java 中, 添加如下方法

/**
 * @author summer
 * @date 2018/4/4 下午5:22
 * @param world 所讲的词
 * @return void
 * @description 用户说话的方法
 */
public Boolean userSay(String world){
    // 对参数判断, 如果参数为空, 则返回false
    if(StringUtils.isEmpty(world)){
        return false;
    }

    // 当传入值是 hello 的时候, 返回 null
    if("hello".equals(world)){
        return null;
    }

    // 执行打印
    System.out.println("user say:" + world);
    // 返回 true
    return true;
}

# 编写测试代码

UserServiceTest.java 中, 添加测试方法如下

/**
 * @author summer
 * @date 2018/4/4 下午5:26
 * @return void
 * @description 用户说话的测试方法
 */
@Test
public void userSay() throws Exception {
    Boolean result = userService.userSay("你好");

    // 断言返回值不为空
    Assert.assertNotNull(result);
    // 断言返回值不为 false
    Assert.assertNotEquals(result, false);
    // 断言返回值是 true
    Assert.assertEquals(result, true);
}

# 代码解释

同上一个测试一样, 我们运行了测试方法 userSay 以后, 测试会报一个绿条, 表示测试通过了.

业务代码逻辑很简单, 就是对传入值进行判断, 如果传入值为空, 返回 false, 如果传入值是 hello 返回 null, 其他的值都执行打印并返回 true

我们再看看测试类中都做了哪些事情, 调用 userSay() 方法, 并获取返回值 result 然后通过 JunitAssert 进行断言. 当然, 这里只是举几个例子, Assert 还有很多其他的方法. 有兴趣的可以自己去挖掘.

# MockMvc 模拟测试

# 编写业务代码

这里我们就直接用上一章讲的 HelloWorldController 做示例. 代码如下:

/**
 * @author summer
 * @date 2018/4/3 下午4:28
 * HelloWorld 程序的 Controller
 */
/*
 * @RestController相当于
 * @Controler+@ResponseBody
 * @Controller表示在Spirng中注入一个bean,这个bean的类型是一个控制器
 * @ResponseBody表示返回的数据类型是JSON
 */
@RestController
/*
 * @RequestMapping用来定义请求的路径,
 * 定义在类上,则当前类下所有的映射路径均有这个前缀
 * 括号内传递的值是所映射的路径
 */
@RequestMapping("/demo")
public class HelloWorldController {
    /**
     * @author summer
     * @date 2018/4/3 下午4:31
     * @return java.lang.String
     * @description HelloWorld 接口
     */
    /*
     * @GetMapping 相当于 @RequestMapping(method = RequestMethod.GET)
     * 同理@PostMapping 相当于 @RequestMapping(method = RequestMethod.POST)
     * 另外还有 @PutMapping @DeleteMapping
     */
    @GetMapping("/hello")
    public String helloWorld(){
        return "Hello World";
    }
}

# 编写测试代码

UserServiceTest.java 同级目录下创建测试类 HelloWorldController.java , 代码如下:

/**
 * @author summer
 * @date 2018/4/4 下午5:02
 * HelloWorld 的测试方法
 */
/*
 * 指定使用的单元测试执行类
 */
@RunWith(SpringRunner.class)
/*
 * 类似springboot程序的测试引导入口
 */
@SpringBootTest
/*
 * 引入模拟器
 * 使 MockMvc 可以自动注入
 */
@AutoConfigureMockMvc
public class HelloWorldControllerTest {

    /**
     * Mock MVC提供了一种强力的方式来测试MVC controllers,而不用启动一个完整的HTTP server
     * 模拟一个 HTTP Server
     */
    @Autowired
    private MockMvc mockMvc;

    /**
     * @author summer
     * @date 2018/4/4 下午5:15
     * @return void
     * @description 使用模拟器测试 `/demo/hello` 接口是否正确
     */
    @Test
    public void helloWorld() throws Exception {
        //设定请求路径及请求方式并执行请求
        MvcResult mvcResult = mockMvc.perform(
                MockMvcRequestBuilders.get("/demo/hello")
                        // 请求时传入的参数
                        .param("name", "value")
                )
                // 判断返回的状态是否ok
                .andExpect(MockMvcResultMatchers.status().isOk())
                // 判断内容是否和预计内容一致
                .andExpect(MockMvcResultMatchers.content().string("Hello World"))
                // 发送请求后需要获取放回时调用
                .andReturn();
    }
}

# 代码解释

# MockMvc解析

1 perform方法其实只是为了构建一个请求,并且返回ResultActions实例,该实例则是可以获取到请求的返回内容。 2 MockMvcRequestBuilders该抽象类则是可以构建多种请求方式,如:PostGetPutDelete等常用的请求方式,其中参数则是我们需要请求的本项目的相对路径,/则是项目请求的根路径。 3 param方法用于在发送请求时携带参数,当然除了该方法还有很多其他的方法,大家可以根据实际请求情况选择调用。 4 andReturn方法则是在发送请求后需要获取放回时调用,该方法返回MvcResult对象,该对象可以获取到返回的视图名称、返回的Response状态、获取拦截请求的拦截器集合等。 5 我们在这里就是使用到了第4步内的MvcResult对象实例获取的MockHttpServletResponse对象从而才得到的Status状态码。 6 同样也是使用MvcResult实例获取的MockHttpServletResponse对象从而得到的请求返回的字符串内容。【可以查看rest返回的json数据】 7 使用Junit内部验证类Assert判断返回的状态码是否正常为200 8 判断返回的字符串是否与我们预计的一样。

通过查看代码中的注解, 好像没有解释的必要了

# 项目源码

SpringBoot 教程: https://github.com/lixian13149999/spring-boot-learn

对应示例项目: test-hello

# 总结

单元测试是软件测试的基础,因此单元测试的效果会直接影响到软件的后期测试,最终在很大程度上影响到产品的质量。从如下几个方面就可以看出单元测试的重要性在何处。

时间方面 如 果认真的做好了单元测试,在系统集成联调时非常顺利,因此会节约很多时间,反之那些由于因为时间原因不做单元测试或随便做做的则在集成时总会遇到那些本应 该在单元测试就能发现的问题,而这种问题在集成时遇到往往很难让开发人员预料到,最后在苦苦寻觅中才发现这是个很低级的错误而在悔恨自己时已经浪费了很多 时间,这种时间上的浪费一点都不值得,正所谓得不偿失。

测试效果 根 据以往的测试经验来看,单元测试的效果是非常明显的,首先它是测试阶段的基础,做好了单元测试,在做后期的集成测试和系统测试时就很顺利。其次在单元测试 过程中能发现一些很深层次的问题,同时还会发现一些很容易发现而在集成测试和系统测试很难发现的问题。再次单元测试关注的范围也特殊,它不仅仅是证明这些 代码做了什么,最重要的是代码是如何做的,是否做了它该做的事情而没有做不该做的事情。

测试成本 在单元测试时某些问题就很容易发现,如果在后期的测试中发现问题所花的成本将成倍数上升。比如在单元测试时发现1个问题需要1个小时,则在集成测试时发现该问题需要2个小时,在系统测试时发现则需要3个小时,同理还有定位问题和解决问题的费用也是成倍数上升的,这就是我们要尽可能早的排除尽可能多的bug来减少后期成本的因素之一。

产品质量 单元测试的好与坏直接影响到产品的质量,可能就是由于代码中的某一个小错误就导致了整个产品的质量降低一个指标,或者导致更严重的后果,如果我们做好了单元测试这种情况是可以完全避免的。

综上所述,单元测试是构筑产品质量的基石,我们不要因为节约单元测试的时间不做单元测试或随便做而让我们在后期浪费太多的不值得的时间,我们也不愿意因为由于节约那些时间导致开发出来的整个产品失败或重来!

# 本文参考文章

https://www.jianshu.com/p/d8f844711bf4

有问题可以在留言区留言, 转发请注明出处, 谢谢

上次更新时间: 2020/6/17 下午5:44:30