引入单元测试

  • Spring Boot中引入单元测试很简单,依赖如下:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>

Service层单元测试

  • Spring Boot中单元测试类写在在src/test/java目录下,你可以手动创建具体测试类,

  • 如果是IDEA,则可以通过IDEA自动创建测试类(Navigate -> Test),也可以通过快捷键⇧⌘T(MAC)或者Ctrl+Shift+T(Window)来创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    package tk.fulsun.demo.service;

    import org.junit.jupiter.api.AfterEach;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;

    /**
    * @Description Service层单元测试
    * @Date 2021/6/13
    * @Created by 凉月-文
    */
    @RunWith(SpringRunner.class)
    @SpringBootTest
    class ServiceATest {
    @Autowired
    private ServiceA serviceA;

    @BeforeEach
    void setUp() {
    System.out.println("start=============");
    }

    @AfterEach
    void tearDown() {
    System.out.println("end=============");

    }

    @Test
    public void test1() {
    serviceA.doSomething();
    System.out.println("--------------测试----------");
    }
    }
  • 上面就是最简单的单元测试写法,顶部只要@RunWith(SpringRunner.class)SpringBootTest即可,想要执行的时候,鼠标放在对应的方法,右键选择run该方法即可。

Controller层单元测试

  • 上面只是针对Service层做测试,但是有时候需要对Controller层(API)做测试,这时候就得用到MockMvc了,你可以不必启动工程就能测试这些接口。
  • MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller的调用,这样可以使得测试速度快、不依赖网络环境,而且提供了一套验证的工具,这样可以使得请求的验证统一而且很方便。

Controller类

  • 用户的controller类如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    package tk.fulsun.demo.controller;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.*;
    import tk.fulsun.demo.mapper.UserMapper;
    import tk.fulsun.demo.model.User;
    import tk.fulsun.demo.model.UserExample;

    import java.util.List;

    /**
    * @Description ControllerA
    * @Date 2021/6/13
    * @Created by 凉月-文
    */
    @RestController
    @RequestMapping("/test")
    public class ControllerA {
    @Autowired
    private UserMapper userMapper;

    @RequestMapping("")
    public String test() {
    return "test-ControllerA";
    }

    @RequestMapping(value = "/queryList", method = RequestMethod.POST)
    @GetMapping("/user/all")
    public List<User> getAllUser() {
    return userMapper.selectByExample(null);
    }

    @GetMapping("/user/{name}")
    public List<User> getUserById(@PathVariable("name") String name) {

    UserExample example = new UserExample();
    UserExample.Criteria criteria = example.createCriteria();
    criteria.andNameLike('%' + name + '%');
    return userMapper.selectByExample(example);
    }

    @PostMapping("/user/add")
    public String addUser(@RequestBody User user) {
    int status = userMapper.insertSelective(user);
    return status > 0 ? "成功" : "失败";
    }

    @PutMapping("/user/update")
    public String updateUser(@RequestBody User user) {
    int status = userMapper.updateByPrimaryKeySelective(user);
    return status > 0 ? "成功" : "失败";
    }

    @DeleteMapping("/user/{id}")
    public String delUser(@PathVariable("id") int id) {
    int status = userMapper.deleteByPrimaryKey(id);
    return status > 0 ? "成功" : "失败";
    }


    }

测试类

  • 这里我们也自动创建一个Controller的测试类,具体代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    import com.alibaba.fastjson.JSON;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.http.MediaType;
    import org.springframework.mock.web.MockHttpSession;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
    import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
    import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
    import org.springframework.test.web.servlet.setup.MockMvcBuilders;
    import org.springframework.web.context.WebApplicationContext;
    import tk.fulsun.demo.model.User;

    import static org.junit.jupiter.api.Assertions.*;

    /**
    * @Description ControllerATest
    * @Date 2021/6/13
    * @Created by 凉月-文
    */
    @RunWith(SpringRunner.class)
    @SpringBootTest
    class ControllerATest {
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mvc;
    private MockHttpSession session;

    @BeforeEach
    public void setupMockMvc() {
    mvc = MockMvcBuilders.webAppContextSetup(wac).build(); //初始化MockMvc对象
    session = new MockHttpSession();
    User user = new User("root", 666);
    session.setAttribute("user", user); //拦截器那边会判断用户是否登录,所以这里注入一个用户
    }


    @Test
    void getAllUser() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/test/user/all")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .session(session))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andDo(MockMvcResultHandlers.print());
    }

    @Test
    void getUserById() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/test/user/1")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .session(session))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("张三"))
    .andExpect(MockMvcResultMatchers.jsonPath("$.age").value(23))
    .andDo(MockMvcResultHandlers.print());
    }

    @Test
    void addUser() throws Exception {
    User user = new User("凉月文", 23);
    String json = JSON.toJSONString(user);
    mvc.perform(MockMvcRequestBuilders.post("/test/user/add")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .content(json.getBytes())
    .session(session))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andDo(MockMvcResultHandlers.print());
    }

    @Test
    void updateUser() throws Exception {
    User user = new User("凉月文", 22);
    user.setId(3);
    String json = JSON.toJSONString(user);
    mvc.perform(MockMvcRequestBuilders.put("/test/user/update")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .content(json.getBytes())
    .session(session))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andDo(MockMvcResultHandlers.print());
    }

    @Test
    void delUser() throws Exception {
    mvc.perform(MockMvcRequestBuilders.delete("/test/user/3")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .session(session))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andDo(MockMvcResultHandlers.print());
    }
    }

MockMvc简单的方法

  • 用下面的代码解释下用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Test
    void getUserById() throws Exception {
    mvc.perform(MockMvcRequestBuilders.get("/test/user/1")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .session(session))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("张三"))
    .andExpect(MockMvcResultMatchers.jsonPath("$.age").value(23))
    .andDo(MockMvcResultHandlers.print());
    }
  • mockMvc.perform执行一个请求

  • MockMvcRequestBuilders.get"/test/user/1")构造一个请求,Post请求就用.post方法

  • contentType(MediaType.APPLICATION_JSON_UTF8)代表发送端发送的数据格式是application/json;charset=UTF-8

  • accept(MediaType.APPLICATION_JSON_UTF8)代表客户端希望接受的数据类型为application/json;charset=UTF-8

  • session(session)注入一个session,这样拦截器才可以通过

  • ResultActions.andExpect添加执行完成后的断言

  • ResultActions.andExpect(MockMvcResultMatchers.status().isOk())方法看请求的状态响应码是否为200如果不是则抛异常,测试不通过

  • andExpect(MockMvcResultMatchers.jsonPath(“$.name”).value("张三"))这里jsonPath用来获取author字段比对是否为张三,不是就测试不通过

  • ResultActions.andDo添加一个结果处理器,表示要对结果做点什么事情,比如此处使用MockMvcResultHandlers.print()输出整个响应结果信息

注解方式创建

  • @AutoConfigureMockMvc //相当于new MockMvc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import org.junit.jupiter.api.Test;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.web.servlet.MockMvc;

    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

    @SpringBootTest
    @AutoConfigureMockMvc
    class MockMvcExampleTests {

    @Test
    void exampleTest(@Autowired MockMvc mvc) throws Exception {
    mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World"));
    }

    }

断言AssertThat

  • JUnit 4.4 结合 Hamcrest 提供了一个全新的断言语法——assertThat。
  • 程序员可以只使用 assertThat 一个断言语句,结合 Hamcrest 提供的匹配符,就可以表达全部的测试思想。

基本语法

1
assertThat( [value], [matcher statement] );
  • value 是接下来想要测试的变量值;
  • matcher statement 是使用 Hamcrest 匹配符来表达的对前面变量所期望的值的声明,如果 value 值与 matcher statement 所表达的期望值相符,则测试成功,否则测试失败。

优点

  • 优点 1:以前 JUnit 提供了很多的 assertion 语句,如:assertEquals,assertNotSame,assertFalse,assertTrue,assertNotNull,assertNull 等,现在有了 JUnit 4.4,一条 assertThat 即可以替代所有的 assertion 语句,这样可以在所有的单元测试中只使用一个断言方法,使得编写测试用例变得简单,代码风格变得统一,测试代码也更容易维护。

  • 优点 2:assertThat 使用了 Hamcrest 的 Matcher 匹配符,用户可以使用匹配符规定的匹配准则精确的指定一些想设定满足的条件,具有很强的易读性,而且使用起来更加灵活。

    • 使用匹配符 Matcher 和不使用之间的比较

      1
      2
      3
      4
      5
      // 想判断某个字符串 s 是否含有子字符串 "developer" 或 "Works" 中间的一个
      // JUnit 4.4 以前的版本:assertTrue(s.indexOf("developer")>-1||s.indexOf("Works")>-1 );
      // JUnit 4.4:
      assertThat(s, anyOf(containsString("developer"), containsString("Works")));
      // 匹配符 anyOf 表示任何一个条件满足则成立,类似于逻辑或 "||", 匹配符 containsString 表示是否含有参数子
  • 优点 3:assertThat 不再像 assertEquals 那样,使用比较难懂的“谓宾主”语法模式(如:assertEquals(3, x);),相反,assertThat 使用了类似于“主谓宾”的易读语法模式(如:assertThat(x,is(3));),使得代码更加直观、易读。

  • 优点 4:可以将这些 Matcher 匹配符联合起来灵活使用,达到更多目的

    1
    2
    3
    4
    5
    6
    // 联合匹配符not和equalTo表示“不等于”
    assertThat( something, not( equalTo( "developer" ) ) );
    // 联合匹配符not和containsString表示“不包含子字符串”
    assertThat( something, not( containsString( "Works" ) ) );
    // 联合匹配符anyOf和containsString表示“包含任何一个子字符串”
    assertThat(something, anyOf(containsString("developer"), containsString("Works")))
  • 优点 5:错误信息更加易懂、可读且具有描述性(descriptive)

    • JUnit 4.4 以前的版本默认出错后不会抛出额外提示信息,如:

      1
      assertTrue( s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
    • 如果该断言出错,只会抛出无用的错误信息,如:junit.framework.AssertionFailedError:null。

    • 如果想在出错时想打印出一些有用的提示信息,必须得程序员另外手动写,如:

      1
      2
      assertTrue( "Expected a string containing 'developer' or 'Works'",
      s.indexOf("developer") > -1 || s.indexOf("Works") > -1 );
    • JUnit 4.4 会默认自动提供一些可读的描述信息,如

      1
      2
      3
      4
      5
      6
      String s = "hello world!";
      assertThat( s, anyOf( containsString("developer"), containsString("Works") ) );
      // 如果出错后,系统会自动抛出以下提示信息:
      java.lang.AssertionError:
      Expected: (a string containing "developer" or a string containing "Works")
      got: "hello world!"

使用 assertThat

  • JUnit 4.4 自带了一些 Hamcrest 的匹配符 Matcher,但是只有有限的几个,在类 org.hamcrest.CoreMatchers 中定义,要想使用他们,必须导入包 org.hamcrest.CoreMatchers.*。

  • 大部分 assertThat 的使用例子:

字符相关匹配符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**equalTo匹配符断言被测的testedValue等于expectedValue,
* equalTo可以断言数值之间,字符串之间和对象之间是否相等,相当于Object的equals方法
*/
assertThat(testedValue, equalTo(expectedValue));

/**equalToIgnoringCase匹配符断言被测的字符串testedString
*在忽略大小写的情况下等于expectedString
*/
assertThat(testedString, equalToIgnoringCase(expectedString));

/**equalToIgnoringWhiteSpace匹配符断言被测的字符串testedString
*在忽略头尾的任意个空格的情况下等于expectedString,
*注意:字符串中的空格不能被忽略
*/
assertThat(testedString, equalToIgnoringWhiteSpace(expectedString);

/**containsString匹配符断言被测的字符串testedString包含子字符串subString**/
assertThat(testedString, containsString(subString) );

/**endsWith匹配符断言被测的字符串testedString以子字符串suffix结尾*/
assertThat(testedString, endsWith(suffix));

/**startsWith匹配符断言被测的字符串testedString以子字符串prefix开始*/
assertThat(testedString, startsWith(prefix));

一般匹配符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**nullValue()匹配符断言被测object的值为null*/
assertThat(object,nullValue());

/**notNullValue()匹配符断言被测object的值不为null*/
assertThat(object,notNullValue());

/**is匹配符断言被测的object等于后面给出匹配表达式*/
assertThat(testedString, is(equalTo(expectedValue)));

/**is匹配符简写应用之一,is(equalTo(x))的简写,断言testedValue等于expectedValue*/
assertThat(testedValue, is(expectedValue));

/**is匹配符简写应用之二,is(instanceOf(SomeClass.class))的简写,
*断言testedObject为Cheddar的实例
*/
assertThat(testedObject, is(Cheddar.class));

/**not匹配符和is匹配符正好相反,断言被测的object不等于后面给出的object*/
assertThat(testedString, not(expectedString));

/**allOf匹配符断言符合所有条件,相当于“与”(&&)*/
assertThat(testedNumber, allOf( greaterThan(8), lessThan(16) ) );

/**anyOf匹配符断言符合条件之一,相当于“或”(||)*/
assertThat(testedNumber, anyOf( greaterThan(16), lessThan(8) ) );

数值相关匹配符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**closeTo匹配符断言被测的浮点型数testedDouble在20.0±0.5范围之内*/
assertThat(testedDouble, closeTo( 20.0, 0.5 ));

/**greaterThan匹配符断言被测的数值testedNumber大于16.0*/
assertThat(testedNumber, greaterThan(16.0));

/** lessThan匹配符断言被测的数值testedNumber小于16.0*/
assertThat(testedNumber, lessThan (16.0));

/** greaterThanOrEqualTo匹配符断言被测的数值testedNumber大于等于16.0*/
assertThat(testedNumber, greaterThanOrEqualTo (16.0));

/** lessThanOrEqualTo匹配符断言被测的testedNumber小于等于16.0*/
assertThat(testedNumber, lessThanOrEqualTo (16.0));

集合相关匹配符

1
2
3
4
5
6
7
8
9
10
11
/**hasEntry匹配符断言被测的Map对象mapObject含有一个键值为"key"对应元素值为"value"的Entry项*/
assertThat(mapObject, hasEntry("key", "value" ) );

/**hasItem匹配符表明被测的迭代对象iterableObject含有元素element项则测试通过*/
assertThat(iterableObject, hasItem (element));

/** hasKey匹配符断言被测的Map对象mapObject含有键值“key”*/
assertThat(mapObject, hasKey ("key"));

/** hasValue匹配符断言被测的Map对象mapObject含有元素值value*/
assertThat(mapObject, hasValue(value));

单元测试的回滚

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @Test
    @Transactional
    void addUser() throws Exception {
    User user = new User("凉月文", 23);
    String json = JSON.toJSONString(user);
    mvc.perform(MockMvcRequestBuilders.post("/test/user/add")
    .contentType(MediaType.APPLICATION_JSON_UTF8)
    .accept(MediaType.APPLICATION_JSON_UTF8)
    .content(json.getBytes())
    .session(session))
    .andExpect(MockMvcResultMatchers.status().isOk())
    .andDo(MockMvcResultHandlers.print());
    }

    // log
    2021-06-13 11:55:40.409 INFO 12340 --- [ main] o.s.t.c.transaction.TransactionContext : Rolled back transaction for test:
  • 这样测试完数据就会回滚了,不会造成垃圾数据。如果你想关闭回滚,只要加上@Rollback(false)注解即可。@Rollback表示事务执行完回滚,支持传入一个参数value,默认true即回滚,false不回滚。

mysql的存储引擎

  • 如果你使用的数据库是Mysql,有时候会发现加了注解@Transactional 也不会回滚,那么你就要查看一下你的默认引擎是不是InnoDB,如果不是就要改成InnoDB。

  • MyISAM与InnoDB是mysql目前比较常用的两个数据库存储引擎,MyISAM与InnoDB的主要的不同点在于性能和事务控制上。这里简单的介绍一下两者间的区别和转换方法:

  • MyISAM:MyISAM是MySQL5.5之前版本默认的数据库存储引擎。MYISAM提供高速存储和检索,以及全文搜索能力,适合数据仓库等查询频繁的应用。但不支持事务、也不支持外键。MyISAM格式的一个重要缺陷就是不能在表损坏后恢复数据。

  • InnoDB:InnoDB是MySQL5.5版本的默认数据库存储引擎,不过InnoDB已被Oracle收购,MySQL自行开发的新存储引擎Falcon将在MySQL6.0版本引进。InnoDB具有提交、回滚和崩溃恢复能力的事务安全。但是比起MyISAM存储引擎,InnoDB写的处理效率差一些并且会占用更多的磁盘空间以保留数据和索引。尽管如此,但是InnoDB包括了对事务处理和外来键的支持,这两点都是MyISAM引擎所没有的。

  • MyISAM适合:(1)做很多count 的计算;(2)插入不频繁,查询非常频繁;(3)没有事务。

  • InnoDB适合:(1)可靠性要求比较高,或者要求事务;(2)表更新和查询都相当的频繁,并且表锁定的机会比较大的情况。(4)性能较好的服务器,比如单独的数据库服务器,像阿里云的关系型数据库RDS就推荐使用InnoDB引擎。

修改默认引擎的步骤

  • 查看MySQL当前默认的存储引擎:

    1
    mysql> show variables like '%storage_engine%';
  • 你要看user表用了什么引擎(在显示结果里参数engine后面的就表示该表当前用的存储引擎):

    1
    mysql> show create table user;
  • 将user表修为InnoDB存储引擎(也可以此命令将InnoDB换为MyISAM):

    1
    mysql> ALTER TABLE user ENGINE=INNODB;
  • 如果要更改整个数据库表的存储引擎,一般要一个表一个表的修改,比较繁琐,可以采用先把数据库导出,得到SQL,把MyISAM全部替换为INNODB,再导入数据库的方式。转换完毕后重启mysql

Mock 测试

  • Mock 测试就是在测试过程中,创建一个假的对象,避免你为了测试一个方法,却要自行构建整个 Bean 的依赖链。

Mockito

  • Mockito 是一种 Java Mock 框架,主要就是用来做 Mock 测试的,它可以模拟任何 Spring 管理的 Bean、模拟方法的返回值、模拟抛出异常等等,同时也会记录调用这些模拟方法的参数、调用顺序,从而可以校验出这个 Mock 对象是否有被正确的顺序调用,以及按照期望的参数被调用。

  • 像是 Mockito 可以在单元测试中模拟一个 Service 返回的数据,而不会真正去调用该 Service,通过模拟一个假的 Service 对象,来快速的测试当前想要测试的类。

  • 目前在 Java 中主流的 Mock 测试工具有 Mockito、JMock、EasyMock等等,而 SpringBoot 目前默认的测试框架是 Mockito 框架。

使用 Mockito

  • test依赖中包含了mockito的依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    </dependency>

service编写

  • 在service中通过Mapper层查询数据库操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Service
    public class ServiceA {
    public void doSomething() {
    System.out.println("ServiceA running .... ");
    }

    @Autowired
    private UserMapper userDao;

    public User getUserById(Integer id) {
    return userDao.selectByPrimaryKey(id);
    }

    public Integer insertUser(User user) {
    return userDao.insert(user);
    }
    }

实际测试

  • 测试代码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package tk.fulsun.demo.controller;

    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mockito;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringRunner;
    import tk.fulsun.demo.mapper.UserMapper;
    import tk.fulsun.demo.model.User;
    import tk.fulsun.demo.service.ServiceA;

    import java.util.List;

    /**
    * @Description Mock测试
    * @Date 2021/6/13
    * @Created by 凉月-文
    */
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class ControllerAMockTest {
    @Autowired
    private ServiceA serviceA ;

    @Test
    public void getUserById() throws Exception {

    //普通的使用userService,他里面会再去调用userDao取得数据库的数据
    User user = serviceA.getUserById(1);
    System.out.println(user);
    //检查结果
    Assert.assertNotNull(user);
    Assert.assertEquals(user.getId(), new Integer(1));
    Assert.assertEquals(user.getName(), "凉月文");
    }

    }

  • 测试结果

模拟UserDao

  • 如果 userDao 还没写好,又想先测 userService 的话,就需要使用 Mockito 去模拟一个假的 userDao 出来。

  • 在 userDao 上加上一个 @MockBean 注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    package tk.fulsun.demo.controller;

    import org.junit.Assert;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.mockito.Mockito;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringRunner;
    import tk.fulsun.demo.mapper.UserMapper;
    import tk.fulsun.demo.model.User;
    import tk.fulsun.demo.service.ServiceA;

    import java.util.List;

    /**
    * @Description Mock测试
    * @Date 2021/6/13
    * @Created by 凉月-文
    */
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class ControllerAMockTest {
    @Autowired
    private ServiceA serviceA ;

    @MockBean
    private UserMapper userDao;

    @Test
    public void getUserById() throws Exception {
    // 定义当调用mock userDao的getUserById()方法,并且参数为3时,就返回id为200、name为I'm mock3的user对象
    Mockito.when(userDao.selectByPrimaryKey(1)).thenReturn(new User("凉月文", 18));

    //普通的使用userService,他里面会再去调用userDao取得数据库的数据
    //返回的会是定义的 user("凉月文", 18)对象
    User user = serviceA.getUserById(1);
    System.out.println(user);
    //检查结果
    Assert.assertNotNull(user);
    Assert.assertEquals(user.getId(), new Integer(1));
    Assert.assertEquals(user.getName(), "凉月文");
    }

    }

  • 测试结果可以看到输出的user对象就是定义的mock对象

Mockito方法

  • 最基本的Mockito.when( 对象.方法名() ).thenReturn( 自定义结果 ),还提供了其他用法让我们使用。

thenReturn

  • 当使用任何整数值调用 userService 的 getUserById() 方法时,就回传一个名字为Aritisan的 User 对象。

    1
    2
    3
    Mockito.when(userService.getUserById(Mockito.anyInt())).thenReturn(new User(3, "Aritisan"));
    User user1 = userService.getUserById(3); // 回传的user的名字为Aritisan
    User user2 = userService.getUserById(200); // 回传的user的名字也为Aritisan
  • 限制只有当参数的数字是 3 时,才会回传名字为 Aritisan 的 user 对象。

    1
    2
    3
    Mockito.when(userService.getUserById(3)).thenReturn(new User(3, "Aritisan"));
    User user1 = userService.getUserById(3); // 回传的user的名字为Aritisan
    User user2 = userService.getUserById(200); // 回传的user为null
  • 当调用 userService 的 insertUser() 方法时,不管传进来的 user 是什么,都回传 100。

    1
    2
    Mockito.when(userService.insertUser(Mockito.any(User.class))).thenReturn(100);
    Integer i = userService.insertUser(new User()); //会返回100

thenThrow

  • 当调用 userService 的 getUserById() 时的参数是 9 时,抛出一个 RuntimeException。

    1
    2
    Mockito.when(userService.getUserById(9)).thenThrow(new RuntimeException("mock throw exception"));
    User user = userService.getUserById(9); //会抛出一个RuntimeException
  • 如果方法没有返回值的话(即是方法定义为 public void myMethod() {…}),要改用 doThrow() 抛出 Exception。

    1
    2
    Mockito.doThrow(new RuntimeException("mock throw  exception")).when(userService).print();
    userService.print(); //会抛出一个RuntimeException

verify

  • 检查调用 userService 的 getUserById()、且参数为3的次数是否为1次。

    1
    Mockito.verify(userService, Mockito.times(1)).getUserById(Mockito.eq(3)) ;
  • 验证调用顺序,验证 userService 是否先调用 getUserById() 两次,并且第一次的参数是 3、第二次的参数是 5,然后才调用insertUser() 方法。

    1
    2
    3
    4
    InOrder inOrder = Mockito.inOrder(userService);
    inOrder.verify(userService).getUserById(3);
    inOrder.verify(userService).getUserById(5);
    inOrder.verify(userService).insertUser(Mockito.any(User.class));

注意事项

  • 不过当使用 Mockito 在 Mock 对象时,有一些限制需要遵守:
    • 不能 Mock 静态方法
    • 不能 Mock private 方法
    • 不能 Mock final class

Test类导入问题

  • pom文件区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <!-- Spring Boot 2.2 之后的 pom.xml -->
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
    <!-- junit-vintage-engine 用于运行JUnit 4 引擎测试
    junit-jupiter-engine 用于运行JUnit 5 引擎测试-->
    <exclusion>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
    </exclusions>
    </dependency>
  • 在代码编写的过程中,Test导包的时候,会有下面二个

    • import org.junit.Test (Junit4)
    • import org.junit.jupiter.api.Test (Junit5)
  • spring boot 2.2之前使用的是 Junit4,类用public 修饰

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class Demo1ApplicationTests {

    @Test
    public void contextLoads() {

    }

    }

If you are using JUnit 4, don’t forget to also add @RunWith(SpringRunner.class) to your test, otherwise the annotations will be ignored. If you are using JUnit 5, there’s no need to add the equivalent @ExtendWith(SpringExtension.class) as @SpringBootTest and the other @…Test annotations are already annotated with it.

  • spring boot 2.2之后使用的是 Junit5,(不需要@RunWith)
1
2
3
4
5
6
7
8
9
10
11
12
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class DemoApplicationTests {

    @Test
    void contextLoads() {

    }

}

JUnit4 与 JUnit 5对比

  • 常用注解

    JUnit4 JUnit5 说明
    @Test @Test 表示该方法是一个测试方法。JUnit5与JUnit 4的@Test注解不同的是,它没有声明任何属性,因为JUnit Jupiter中的测试扩展是基于它们自己的专用注解来完成的。这样的方法会被继承,除非它们被覆盖
    @BeforeClass @BeforeAll 表示使用了该注解的方法应该在当前类中所有使用了@Test @RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前 执行;
    @AfterClass @AfterAll 表示使用了该注解的方法应该在当前类中所有使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后执行;
    @Before @BeforeEach 表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之前 执行
    @After @AfterEach 表示使用了该注解的方法应该在当前类中每一个使用了@Test、@RepeatedTest、@ParameterizedTest或者@TestFactory注解的方法之后 执行
    @Ignore @Disabled 用于禁用一个测试类或测试方法
    @Category @Tag 用于声明过滤测试的tags,该注解可以用在方法或类上;类似于TesgNG的测试组或JUnit 4的分类。
    @Parameters @ParameterizedTest 表示该方法是一个参数化测试
    @RunWith @ExtendWith @Runwith就是放在测试类名之前,用来确定这个类怎么运行的
    @Rule @ExtendWith Rule是一组实现了TestRule接口的共享类,提供了验证、监视TestCase和外部资源管理等能力
    @ClassRule @ExtendWith @ClassRule用于测试类中的静态变量,必须是TestRule接口的实例,且访问修饰符必须为public。

Springboot集成JUnit5

为什么使用JUnit5

  • JUnit4被广泛使用,但是许多场景下使用起来语法较为繁琐,JUnit5中支持lambda表达式,语法简单且代码不冗余。
  • JUnit5易扩展,包容性强,可以接入其他的测试引擎。
  • 功能更强大提供了新的断言机制、参数化测试、重复性测试等新功能。
  • ps:开发人员为什么还要测试,单测写这么规范有必要吗?其实单测是开发人员必备技能,只不过很多开发人员开发任务太重导致调试完就不管了,没有系统化得单元测试,单元测试在系统重构时能发挥巨大的作用,可以在重构后快速测试新的接口是否与重构前有出入。

JUnit5结构如下

  • JUnit Platform: 这是Junit提供的平台功能模块,通过它,其它的测试引擎都可以接入Junit实现接口和执行。
  • JUnit JUpiter:这是JUnit5的核心,是一个基于JUnit Platform的引擎实现,它包含许多丰富的新特性来使得自动化测试更加方便和强大。
  • JUnit Vintage:这个模块是兼容JUnit3、JUnit4版本的测试引擎,使得旧版本的自动化测试也可以在JUnit5下正常运行。

依赖引入

  • 我们以SpringBoot2.3.1为例,引入如下依赖,防止使用旧的junit4相关接口我们将其依赖排除。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
    <exclusion>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    </exclusion>
    </exclusions>
    </dependency>

常用注解

  • @BeforeEach:在每个单元测试方法执行前都执行一遍
  • @BeforeAll:在每个单元测试方法执行前执行一遍(只执行一次)
  • @DisplayName(“商品入库测试”):用于指定单元测试的名称
  • @Disabled:当前单元测试置为无效,即单元测试时跳过该测试
  • @RepeatedTest(n):重复性测试,即执行n次
  • @ParameterizedTest:参数化测试,
  • @ValueSource(ints = {1, 2, 3}):参数化测试提供数据

断言

  • JUnit Jupiter提供了强大的断言方法用以验证结果,在使用时需要借助java8的新特性lambda表达式,均是来自org.junit.jupiter.api.Assertions包的static方法。

  • assertTrueassertFalse用来判断条件是否为truefalse

    1
    2
    3
    4
    5
    @Test
    @DisplayName("测试断言equals")
    void testEquals() {
    assertTrue(3 < 4);
    }
  • assertNullassertNotNull用来判断条件是否为null

    1
    2
    3
    4
    5
    @Test
    @DisplayName("测试断言NotNull")
    void testNotNull() {
    assertNotNull(new Object());
    }
  • assertThrows用来判断执行抛出的异常是否符合预期,并可以使用异常类型接收返回值进行其他操作

    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    @DisplayName("测试断言抛异常")
    void testThrows() {
    ArithmeticException arithExcep = assertThrows(ArithmeticException.class, () -> {
    int m = 5/0;
    });
    assertEquals("/ by zero", arithExcep.getMessage());
    }
  • assertTimeout用来判断执行过程是否超时

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Test
    @DisplayName("测试断言超时")
    void testTimeOut() {
    String actualResult = assertTimeout(ofSeconds(2), () -> {
    Thread.sleep(1000);
    return "a result";
    });
    System.out.println(actualResult);
    }
  • assertAll是组合断言,当它内部所有断言正确执行完才算通过

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Test
    @DisplayName("测试组合断言")
    void testAll() {
    assertAll("测试item商品下单",
    () -> {
    //模拟用户余额扣减
    assertTrue(1 < 2, "余额不足");
    },
    () -> {
    //模拟item数据库扣减库存
    assertTrue(3 < 4);
    },
    () -> {
    //模拟交易流水落库
    assertNotNull(new Object());
    }
    );
    }

重复性测试

  • 在许多场景中我们需要对同一个接口方法进行重复测试,例如对幂等性接口的测试。

  • JUnit Jupiter通过使用@RepeatedTest(n)指定需要重复的次数

    1
    2
    3
    4
    5
    @RepeatedTest(3)
    @DisplayName("重复测试")
    void repeatedTest() {
    System.out.println("调用");
    }

参数化测试

  • 参数化测试可以按照多个参数分别运行多次单元测试这里有点类似于重复性测试,只不过每次运行传入的参数不用。

  • 需要使用到@ParameterizedTest,同时也需要@ValueSource提供一组数据,它支持八种基本类型以及String和自定义对象类型,使用极其方便。

    1
    2
    3
    4
    5
    6
    @ParameterizedTest
    @ValueSource(ints = {1, 2, 3})
    @DisplayName("参数化测试")
    void paramTest(int a) {
    assertTrue(a > 0 && a < 4);
    }

内嵌测试

  • JUnit5提供了嵌套单元测试的功能,可以更好展示测试类之间的业务逻辑关系,我们通常是一个业务对应一个测试类,有业务关系的类其实可以写在一起。这样有利于进行测试。而且内联的写法可以大大减少不必要的类,精简项目,防止类爆炸等一系列问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    @SpringBootTest
    @AutoConfigureMockMvc
    @DisplayName("Junit5单元测试")
    public class MockTest {
    //....
    @Nested
    @DisplayName("内嵌订单测试")
    class OrderTestClas {
    @Test
    @DisplayName("取消订单")
    void cancelOrder() {
    int status = -1;
    System.out.println("取消订单成功,订单状态为:"+status);
    }
    }
    }

参考