Spring Boot中的单元和集成测试

了解如何在Spring Boot环境中编写单元和集成测试,以及在本教程中为此提供便利的工具,本文还会提供一种工具来帮助我们写单元和集成测试。

1 概述

在这篇文章中,我们将了解如何在Spring Boot环境中编写测试单元和集成。您可以在线找到大量有关此主题的教程,但很难在一个页面中找到所需的所有信息。我经常注意到初级开发人员在单元和集成测试之间混淆,特别是在谈到spring生态系统时,我会尝试解释说明在不同环境中使用的不同注释的用法。

2. Unit vs. Integration tests

unit testing维基百科中的解释:在计算机编程中,单元测试是一种软件测试方法,通过该方法测试各个源代码单元,一个或多个计算机程序模块的集合以及相关的控制数据,使用程序和操作程序,以确定它们是否适合使用。

Integration testing:集成测试(有时称为集成和测试,缩写为I&T)是软件测试阶段,其中各个软件模块组合并作为一组进行测试。

简单来说,当我们进行单元测试时,我们只测试一个代码单元,一次测试一个方法,排除对测试有影响的所有其他组件。

在另一方面的集成测试中,我们测试组件之间的集成。由于单元测试,我们知道组件的行为与所需的一样,但我们不知道它们将如何完全工作。这是集成测试的责任。

3. Java Test Unit

所有Java开发人员都知道JUnitas是执行测试单元的主要框架。它提供了许多注释来对期望做出断言。
Hamcrest是另一个软件测试框架。Hamcrest允许使用现有匹配器类检查代码中的条件,还允许您定义自定义匹配器实现。要在JUnit中使用Hamcrest匹配器,您必须使用assertThat语句,后跟一个或多个匹配器。
在这里,您可以看到使用两个框架的简单测试:

package edu.princeton.cs.algs4;

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;

import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;

public class AssertTests {
    @Test
    public void testAssertArrayEquals() {
        byte[] expected = "trial".getBytes();
        byte[] actual = "trial".getBytes();
        assertArrayEquals("failure - byte arrays not same", expected, actual);
    }

    @Test
    public void testAssertEquals() {
        assertEquals("failure - strings are not equal", "text", "text");
    }

    @Test
    public void testAssertFalse() {
        assertFalse("failure - should be false", false);
    }

    @Test
    public void testAssertNotNull() {
        assertNotNull("should not be null", new Object());
    }

    @Test
    public void testAssertNotSame() {
        assertNotSame("should not be same Object", new Object(), new Object());
    }

    @Test
    public void testAssertNull() {
        assertNull("should be null", null);
    }

    @Test
    public void testAssertSame() {
        Integer aNumber = Integer.valueOf(768);
        assertSame("should be same", aNumber, aNumber);
    } // JUnit Matchers assertThat

    @Test
    public void testAssertThatBothContainsString() {
        assertThat("albumen", both(containsString("a")).and(containsString("b")));
    }

    @Test
    public void testAssertThatHasItems() {
        assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
    }

    @Test
    public void testAssertThatEveryItemContainsString() {
        assertThat(Arrays.asList(new String[]{"fun", "ban", "net"}), everyItem(containsString("n")));
    } // Core Hamcrest Matchers with assertThat

    @Test
    public void testAssertThatHamcrestCoreMatchers() {
        assertThat("good", allOf(equalTo("good"), startsWith("good")));
        assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
        assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
        assertThat(7, not(CombinableMatcher.either(equalTo(3)).or(equalTo(4))));
        assertThat(new Object(), not(sameInstance(new Object())));
    }

    @Test
    public void testAssertTrue() {
        assertTrue("failure - should be true", true);
    }
}

4. Introducing Our Example

让我们编写简单的应用程序。这个想法是为漫画提供一个基本的搜索引擎。

4.1. Maven Dependencies

首先,我们必须给我们的项目添加一些依赖:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <version>1.16.20</version>
  <scope>provided</scope>
</dependency>

4.2. Define the Model

我们的模型非常简单;它只由两个类组成:Manga和MangaResult。

4.2.1. Manga Class

Manga类表示由系统检索的漫画实例。我使用Lombok来减少样板代码。

package com.mgiglione.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Manga {
    private String title;
    private String description;
    private Integer volumes;
    private Double score;
}

4.2.2. MangaResult

MangaResult是一个包装类,包含一个漫画列表。

package com.mgiglione.model;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter @Setter @NoArgsConstructor
public class MangaResult {
    private List<Manga> result;
}

4.3. Implementing the Service

为了实现该服务,我们将使用Jikan Moe自由公开的API。
RestTemplate是我用来对API进行REST调用的Spring类。

package com.mgiglione.service;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;
@Service
public class MangaService {
    Logger logger = LoggerFactory.getLogger(MangaService.class);
    private static final String MANGA_SEARCH_URL="http://api.jikan.moe/search/manga/";
    @Autowired
    RestTemplate restTemplate;
    public List<Manga> getMangasByTitle(String title) {
        return restTemplate.getForEntity(MANGA_SEARCH_URL+title, MangaResult.class).getBody().getResult();
    }
}

4.4. Implementing the Controller

列表的下一步是写下暴露两个端点的REST Controller,一个是同步的,一个是异步的,仅用于测试目的。该控制器使用上面定义的Service。

package com.mgiglione.controller;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@RestController
@RequestMapping(value = "/manga")
public class MangaController {
    Logger logger = LoggerFactory.getLogger(MangaController.class);
    @Autowired
    private MangaService mangaService;   
    @RequestMapping(value = "/async/{title}", method = RequestMethod.GET)
    @Async
    public CompletableFuture<List<Manga>> searchASync(@PathVariable(name = "title") String title) {
        return CompletableFuture.completedFuture(mangaService.getMangasByTitle(title));
    }
    @RequestMapping(value = "/sync/{title}", method = RequestMethod.GET)
    public @ResponseBody <List<Manga>> searchSync(@PathVariable(name = "title") String title) {
        return mangaService.getMangasByTitle(title);
    }
}

4.5. Launching and Testing the System

mvn spring-boot:run

Then let’s try it:

curl http://localhost:8080/manga/async/ken
curl http://localhost:8080/manga/sync/ken

Example of output:

{  
   "title":"Rurouni Kenshin: Meiji Kenkaku Romantan",
   "description":"Ten years have passed since the end of Bakumatsu, an era of war that saw the uprising of citizens against the Tokugawa shogunate. The revolutionaries wanted to create a time of peace, and a thriving c...",
   "volumes":28,
   "score":8.69
},
{  
   "title":"Sun-Ken Rock",
   "description":"The story revolves around Ken, a man from an upper-class family that was orphaned young due to his family's involvement with the Yakuza; he became a high school delinquent known for fighting. The only...",
   "volumes":25,
   "score":8.12
},
{  
   "title":"Yumekui Kenbun",
   "description":"For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....",
   "volumes":9,
   "score":7.97
}

5. Unit Testing the Spring Boot Application

Spring boot提供一个很棒的类是测试变得简单: @SpringBootTest annotation

可以在运行基于Spring Boot的测试的测试类上指定此批注。除常规Spring TestContext框架之外,还提供以下功能:

  • 当没有定义特定的@ContextConfiguration(loader = …)时,使用SpringBootContextLoader作为默认的ContextLoader。
  • 在未使用嵌套的@Configuration和未指定显式类时自动搜索@SpringBootConfiguration。
  • 允许使用properties属性定义自定义环境属性。
  • 支持不同的web环境模型,包括启动在定义或随机端口上侦听的完全运行的Web服务器的功能。
  • 注册TestRestTemplate和/或WebTestClient bean,以便在使用完全运行的Web服务器的Web测试中使用。

我们在这里测试基本上有两个组件:MangaService和MangaController

5.1. Unit Testing MangaService

为了测试MangaService,我们需要将其与外部组件隔离。在本例子,我们只需要一个外部组件:RestTemplate,我们用它来调用远程API。
我们需要做的是模拟RestTemplate bean并让它始终以固定的给定响应进行响应。 Spring Test结合并扩展了Mockito库,通过@MockBean注释配置模拟bean。

package com.mgiglione.service.test.unit;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;
import com.mgiglione.service.MangaService;
import com.mgiglione.utils.JsonUtils;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MangaServiceUnitTest {
    @Autowired
    private MangaService mangaService;
    // MockBean is the annotation provided by Spring that wraps mockito one
    // Annotation that can be used to add mocks to a Spring ApplicationContext.
    // If any existing single bean of the same type defined in the context will be replaced by the mock, if no existing bean is defined a new one will be added.
    @MockBean
    private RestTemplate template;
    @Test
    public void testGetMangasByTitle() throws IOException {
        // Parsing mock file
        MangaResult mRs = JsonUtils.jsonFile2Object("ken.json", MangaResult.class);
        // Mocking remote service
        when(template.getForEntity(any(String.class), any(Class.class))).thenReturn(new ResponseEntity(mRs, HttpStatus.OK));
        // I search for goku but system will use mocked response containing only ken, so I can check that mock is used.
        List<Manga> mangasByTitle = mangaService.getMangasByTitle("goku");
        assertThat(mangasByTitle).isNotNull()
            .isNotEmpty()
            .allMatch(p -> p.getTitle()
                .toLowerCase()
                .contains("ken"));
    }
}

5.2. Unit Testing MangaController

正如在服务的单元测试中所做的那样,我们需要隔离组件。在这种情况下,我们需要模拟MangaService bean。
然后,我们还有一个问题……Controller部分是管理HttpRequest的系统的一部分,因此我们需要一个系统来模拟这种行为,而无需启动完整的HTTP服务器。
MockMvc就是这样做的Spring类。可以用不同的方式设置它:

  1. 使用 Standalone Context
  2. 使用 WebApplication Context
  3. spring通过在测试类上使用@SpringBootTest @AutoConfigureMockMvc注解自动加载所有上下文来自动配置它
  4. 让Spring通过在测试类@WebMvcTest上使用这些注释加载Web层上下文来自动配置它
package com.mgiglione.service.test.unit;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import java.util.ArrayList;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
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.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;
import com.mgiglione.controller.MangaController;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MangaControllerUnitTest {
    MockMvc mockMvc;
    @Autowired
    protected WebApplicationContext wac;
    @Autowired
    MangaController mangaController;
    @MockBean
    MangaService mangaService;
    /**
     * List of samples mangas
     */
    private List<Manga> mangas;
    @Before
    public void setup() throws Exception {
        this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
        // mockMvc = MockMvcBuilders.webAppContextSetup(wac)
        // .build();
        Manga manga1 = Manga.builder()
            .title("Hokuto no ken")
            .description("The year is 199X. The Earth has been devastated by nuclear war...")
            .build();
        Manga manga2 = Manga.builder()
            .title("Yumekui Kenbun")
            .description("For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....")
            .build();
        mangas = new ArrayList<>();
        mangas.add(manga1);
        mangas.add(manga2);
    }
    @Test
    public void testSearchSync() throws Exception {
        // Mocking service
        when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
        mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].title", is("Hokuto no ken")))
            .andExpect(jsonPath("$[1].title", is("Yumekui Kenbun")));
    }
    @Test
    public void testSearchASync() throws Exception {
        // Mocking service
        when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
        MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(request().asyncStarted())
            .andDo(print())
            // .andExpect(status().is2xxSuccessful()).andReturn();
            .andReturn();
        // result.getRequest().getAsyncContext().setTimeout(10000);
        mockMvc.perform(asyncDispatch(result))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].title", is("Hokuto no ken")));
    }
}

正如代码中所见,我选择第一个方法的原因是它是最轻的一个,而且对Spring上下文中加载的内容有最好的管理。

在异步测试中,我必须通过首先调用服务然后启动asyncDispatch方法来模拟异步行为。

6. Integration Testing the Spring Boot Application

对于集成测试,我们希望通过下游通信检查我们的主要组件。

6.1. Integration Testing of MangaService

这个测试非常简单。我们不需要模拟任何东西因为我们只需要调用远程的 mangas API.

package com.mgiglione.service.test.integration;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.junit.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;
import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MangaServiceIntegrationTest {
    @Autowired
    private MangaService mangaService;
    @Test
    public void testGetMangasByTitle() {
           List<Manga> mangasByTitle = mangaService.getMangasByTitle("ken");
           assertThat(mangasByTitle).isNotNull().isNotEmpty();
    }
}

6.2. Integration Testing of MangaController

测试与单元测试非常相似,但在这种情况下,我们不需要没有模拟服务。

package com.mgiglione.service.test.integration;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
import org.junit.Before;
import org.junit.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.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.web.context.WebApplicationContext;
import com.mgiglione.controller.MangaController;
@SpringBootTest
@RunWith(SpringRunner.class)
public class MangaControllerIntegrationTest {
    // @Autowired
    MockMvc mockMvc;
    @Autowired
    protected WebApplicationContext wac;
    @Autowired
    MangaController mangaController;
    @Before
    public void setup() throws Exception {
        this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
        // mockMvc = MockMvcBuilders.webAppContextSetup(wac)
        // .build();
    }
    @Test
    public void testSearchSync() throws Exception {
        mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
    }
    @Test
    public void testSearchASync() throws Exception {
        MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
            .andDo(print())
            .andExpect(request().asyncStarted())
            .andDo(print())
            .andReturn();
        mockMvc.perform(asyncDispatch(result))
            .andDo(print())
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
    }
}

7 结束语

我们已经看到了Spring Boot环境中单元测试和集成测试之间的主要区别,了解像Hamcrest这样简化测试编写的框架。当然,您可以在我的GitHub存储库中找到所有内容。

猜你喜欢

转载自blog.csdn.net/duiop99/article/details/81213058