【测试相关】Spring Boot测试介绍,与JUnit4, Junit5的集成

本文主要介绍Spring Boot的测试框架——spring-boot-starter-test模块,主要内容分为两块:

  • 第一部分是与JUnit测试的集成:

    与JUnit测试的集成

  • 第二部分介绍了与测试相关的一系列注解:

    很多注解

1. Spring Boot的核心测试模块

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

2. Junit4和Junit5

Spring Boot版本在2.4之后就不支持JUnit4了。

首先是2.3.12.RELEASE依赖:mvnrepository地址

2.3.12.RELEASE依赖

可以看到在2.4以前,除了引入了junit-jupiter之外,还引用了junit-vintage-engine。
而junit-vintage-engine最大的特点就是它运行JUnit 4引擎测试,mvnrepository地址

mvnrepository地址

再看2.4.0依赖:mvnrepository地址

2.4.0依赖

而junit-juspiter用的是JUnit 5引擎测试,mvnrepository地址

image.png

总结下就是:

  • vintage的意思是古典的,即使用的是junit4的引擎
  • jupiter的意思是木星,是junit5的group id,即junit-jupiter。
Spring Boot Release 引用的Junit 引擎 运行器/功能扩展器
2.3.12.RELEASE junit-jupiter, junit-vintage-engine junit 4.13(junit4引擎测试) @RunWith(SpringRunner.class)
2.4.0(+) junit-jupiter junit-jupiter-engine(junit 5引擎测试) @ExtendWith(SpringExtension.class)

如果在Spring Boot高版本(>=2.4.0)使用Junit4,那么需要手动加上junit-vintage-engine依赖。
Spring Boot高版本(>=2.4.0)不需要在@SpringBootTest加上之后额外再加上@ExtendWith(SpringExtension.class) 的原因是@SpringBootTest已经帮我们加上了,具体看@SpringBootTest v2.4.0版本API

3. 使用@SpringBootTest集成测试

@SpringBootTest注解是用来运行Spring整个容器的,它会创建一个ApplicationContext(主要是通过查找注解@SpringBootApplication入口,即Spring Boot启动类)。

3.1 测试类中可以使用@Autowired拿到相应的bean
@SpringBootTest
public class UserServiceTest {
    @Autowired
    private UserService userService;

    @Test
    public void getUserService() {
        Assertions.assertNotNull(userService);
    }
}
3.2 @SpringBootTest注解还可以指定WebEnvironment

首先新建一个API,/version,返回“v1.0”。

测试类,使用RANDOM_PORT会随机生成端口,可以有效的避免端口冲突:

  • 使用@LocalServerPort获取到随机生成的端口。
  • 默认注入了TestRestTemplate,可以直接拿来使用。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class VersionControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate testRestTemplate;

    @Test
    public void test() {
        String version = testRestTemplate.getForObject("http://localhost:" + port + "/version", String.class);
        Assertions.assertEquals("v1.0", version);
    }
}
3.3 使用@TestPropertySource读取测试配置

默认的配置 - application.properties:

hello.name=spring boot test
hello.env=dev

Test独有的配置 - test.properties:

hello.env=test
hello.testonly=true

@TestPropertySource读取配置后,优先级是最高的,比如hello.env最后的值是test,而不是dev。

@SpringBootTest
@TestPropertySource(locations = "classpath:test.properties")
public class PropertySourceTest {

    @Value(value = "${hello.env}")
    private String helloEnv;

    @Value(value = "${hello.name}")
    private String helloName;

    @Value(value = "${hello.testonly}")
    private boolean helloTestOnly;

    @Test
    public void test() {
        Assertions.assertEquals("test", helloEnv);
        Assertions.assertEquals("spring boot test", helloName);
        Assertions.assertEquals(true, helloTestOnly);
    }
}

4. Test Configuration:@TestConfiguration

首先创建一个简单的Class类,叫Course,属性有id和name。

4.1 @TestConfiguration作为内部类

通过@TestConfiguration创建的Bean,作用范围在test里,不会影响到正式环境。通过@Autowired可以拿到创建的bean:

@SpringBootTest
public class CourseInnerClassTest {

    @TestConfiguration
    static class CourseConfig {
        @Bean
        public Course course() {
            return Course.builder().id(1).name("test01").build();
        }
    }

    @Autowired
    private Course course;

    @Test
    public void test() {
        Assertions.assertEquals(1, course.getId());
        Assertions.assertEquals("test01", course.getName());
    }
}
4.2 @TestConfiguration作为独立的配置类
@TestConfiguration
public class CourseConfig {
    @Bean
    public Course course2() {
        return Course.builder().id(2).name("test02").build();
    }
}

测试,使用@Import把配置类导入进来,或者也可使用@SpringBootTest(classes = CourseConfig.class)导入配置类:

@SpringBootTest
@Import(CourseConfig.class)
public class CourseSeparateClassTest {
    @Autowired
    private Course course2;

    @Test
    public void test() {
        Assertions.assertEquals(2, course2.getId());
        Assertions.assertEquals("test02", course2.getName());
    }
}

另外,如果@Configuration有定义的bean,在@TestConfiguration中想要覆盖,可以预先开启spring.main.allow-bean-definition-overriding=true,这样就可以使用@TestConfiguration覆盖@Configuration中的bean了。

5. 与Mockito结合使用——@MockBean

Spring Boot Starter Test引入了mockito框架:

Spring Boot Test依赖

使用@MockBean注解来生成一个mock的bean,我们可以使用Mockito.when来模拟一些方法(比如Mock Jpa的Repository的find方法,这样就算数据库里的数据还没有准备好,我们也可以自己模拟数据了。)

@SpringBootTest
public class CourseServiceTest {
    @MockBean
    private CourseService courseService;

    @Test
    public void test() {
        Mockito.when(courseService.getName()).thenReturn("mock name");
        Assertions.assertEquals("mock name", courseService.getName());
    }
}

6. 使用@DataJpaTest 注解测试JPA

首先需要加入h2的依赖,scope是test:

        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <version>2.1.212</version>
            <scope>test</scope>
        </dependency>

@DataJpaTest注解主要关注的是JPA的component。使用这个注解不会开始全部的auto-configuration,而是只会生效JPA相关的测试。(这就意味着上述的CourseService并不会成功的被Autowired。)

@DataJpaTest会配置:

  • 默认情况下,带有@DataJpaTest注解的测试使用嵌入式内存数据库。会基于h2生成自己的DataSource,如果别的什么都没有配,就是加了这个注解,运行的话,可以看到h2的JDBC Url也是随机生成的。
  • 除了DataSourse,还会生成Hibernate以及Spring Data相关的配置(如TestEntityManager,或是下文的UserRepository)。
  • 会自动加上@EntityScan,自动扫描Hibernate相关的Entity。
  • 开启SQL日志(类似spring.jpa.show-sql=true)

启动日志,H2 JDBC Url是随机的:

2022-05-28 20:14:09.241 INFO 14181 --- [ main] o.s.j.d.e.EmbeddedDatabaseFactory : Starting embedded database: url='jdbc:h2:mem:6d67e11c-0e76-415f-a6e4-11825b7c214a;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'

以下是测试示例,我加了@TestPropertySource来set了sql的格式,让他可以换行显示。

@DataJpaTest
@TestPropertySource(properties = {
        "spring.jpa.properties.hibernate.format_sql=true"
})
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    public void test() {
        User user = new User();
        user.setId(1);
        user.setName("test001");
        userRepository.save(user);

        List<User> list = userRepository.findAll();
        Assertions.assertEquals(1, list.size());
    }
}

如果不想要用测试生成的h2,而是想要使用代码中配置的DataSource,可以加上@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE),这个配置的意思是不会覆盖app默认的DataSource,即如果原本配的是MySQL数据库的DataSource,那么测试的时候也会去MySQL中操作数据。

7. 使用@WebMvcTest

上述的:
@SpringBootTest聚集的是整个Spring框架的集成测试。
@DataJpaTest让我们更好的聚焦JPA层的测试(通过h2内嵌数据库来测试)。
@WebMvcTest让我们关注的是Controller层的测试。它会自动生成Spring MVC框架相关的配置。它可以让我们测试应用的Web层行为是否正确,并且不想卷入数据库调用。

以下是示例:
首先是Controller:

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping
    public @ResponseBody List<User> listUser() {
        return userService.list();
    }
}

测试类:
@WebMvcTest位于spring-boot-test-autoconfigure包中,传入的Controller类相当于是测试目标类。因为它只会激活这个Controller的相关配置(API等),所以UserController中的UserService并不会被扫描到并且注入,所以需要使用@MockBean来mock。

@MockMvc相关的类(包括MockMvcRequestBuilders等)位于spring-test包中,是spring框架中重要的子框架之一。

  • MockMvcRequestBuilders:提供了很重要的static方法如get(URI), post(URI), put(URI), delete(URI)等。很多人会在import的时候声明到get方法,这样就可以在代码中直接使用get(URI)了。
  • MockMvcResultMatchers:用来匹配执行完成后的结果,其中的status(), jsonPath()也都是静态方法。
@WebMvcTest(UserController.class)
public class UserControllerTest {
    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserService userService;

    @Test
    public void test_list() throws Exception {
        List<User> result = new ArrayList<>();
        result.add(new User(1, "user01"));
        result.add(new User(2, "user02"));

        Mockito.when(userService.list()).thenReturn(result);

        mvc.perform(MockMvcRequestBuilders.get("/users"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$[0]['id']").value(1))
                .andExpect(MockMvcResultMatchers.jsonPath("$[0]['name']").value("user01"));
    }
}

注:@WebMvcTest不能与@SpringBootTest一起用,否则就会报错:Configuration error: found multiple declarations of @BootstrapWith。

8. 其它Spring boot test auto-configure包中的注解

  • @WebFluxTest
  • @JdbcTest
  • @JooqTest
  • @DataMongoTest
  • @DataRedisTest
  • @DataLdapTest
  • @RestClientTest
  • @JsonTest

参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,752评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,100评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,244评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,099评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,210评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,307评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,346评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,133评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,546评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,849评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,019评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,702评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,331评论 3 319
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,030评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,260评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,871评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,898评论 2 351

推荐阅读更多精彩内容