Spring Boot 单元测试和集成测试实现详解
学习如何使用本教程中提供的工具,并在SpringBoot环境中编写单元测试和集成测试。
1.概览
本文中,我们将了解如何编写单元测试并将其集成在SpringBoot环境中。你可在网上找到大量关于这个主题的教程,但很难在一个页面中找到你需要的所有信息。我经常注意到初级开发人员混淆了单元测试和集成测试的概念,特别是在谈到Spring生态系统时。我将尝试讲清楚不同注解在不同上下文中的用法。
2.单元测试vs.集成测试
维基百科是这么说单元测试的:
在计算机编程中,单元测试是一种软件测试方法,用以测试源代码的单个单元、一个或多个计算机程序模块的集合以及相关的控制数据、使用过程和操作过程,以确定它们是否适合使用。
集成测试:
“集成测试(有时也称集成和测试,缩写为I&T)是软件测试的一个阶段,在这个阶段中,各个软件模块被组合在一起来进行测试。”
简而言之,当我们在做单元测试时,只是测试了一个代码单元,每次只测试一个方法,不包括与正测试组件相交互的其他所有组件。
另一方面,在集成测试中,我们测试各组件之间的集成。由于单元测试,我们可知这些组件行为与所需一致,但不清楚它们是如何在一起工作的。这就是集成测试的职责。
3.Java单元测试
所有Java开发者都知道JUnit是执行单元测试的主要框架。它提供了许多注解来对期望进行断言。
Hamcrest是一个用于软件测试的附加框架。Hamcrest允许使用现有的matcher类来检查代码中的条件,还允许自定义matcher实现。要在JUnit中使用Hamcrestmatcher,必须使用assertThat语句,后跟一个或多个matcher。
在这里,你可以看到使用这两种框架的简单测试:
importstaticorg.hamcrest.CoreMatchers.allOf;
importstaticorg.hamcrest.CoreMatchers.anyOf;
importstaticorg.hamcrest.CoreMatchers.both;
importstaticorg.hamcrest.CoreMatchers.containsString;
importstaticorg.hamcrest.CoreMatchers.equalTo;
importstaticorg.hamcrest.CoreMatchers.everyItem;
importstaticorg.hamcrest.CoreMatchers.hasItems;
importstaticorg.hamcrest.CoreMatchers.not;
importstaticorg.hamcrest.CoreMatchers.sameInstance;
importstaticorg.hamcrest.CoreMatchers.startsWith;
importstaticorg.junit.Assert.assertArrayEquals;
importstaticorg.junit.Assert.assertEquals;
importstaticorg.junit.Assert.assertFalse;
importstaticorg.junit.Assert.assertNotNull;
importstaticorg.junit.Assert.assertNotSame;
importstaticorg.junit.Assert.assertNull;
importstaticorg.junit.Assert.assertSame;
importstaticorg.junit.Assert.assertThat;
importstaticorg.junit.Assert.assertTrue;
importjava.util.Arrays;
importorg.hamcrest.core.CombinableMatcher;
importorg.junit.Test;
publicclassAssertTests{
@Test
publicvoidtestAssertArrayEquals(){
byte[]expected="trial".getBytes();
byte[]actual="trial".getBytes();
assertArrayEquals("failure-bytearraysnotsame",expected,actual);
}
@Test
publicvoidtestAssertEquals(){
assertEquals("failure-stringsarenotequal","text","text");
}
@Test
publicvoidtestAssertFalse(){
assertFalse("failure-shouldbefalse",false);
}
@Test
publicvoidtestAssertNotNull(){
assertNotNull("shouldnotbenull",newObject());
}
@Test
publicvoidtestAssertNotSame(){
assertNotSame("shouldnotbesameObject",newObject(),newObject());
}
@Test
publicvoidtestAssertNull(){
assertNull("shouldbenull",null);
}
@Test
publicvoidtestAssertSame(){
IntegeraNumber=Integer.valueOf(768);
assertSame("shouldbesame",aNumber,aNumber);
}
//JUnitMatchersassertThat
@Test
publicvoidtestAssertThatBothContainsString(){
assertThat("albumen",both(containsString("a")).and(containsString("b")));
}
@Test
publicvoidtestAssertThatHasItems(){
assertThat(Arrays.asList("one","two","three"),hasItems("one","three"));
}
@Test
publicvoidtestAssertThatEveryItemContainsString(){
assertThat(Arrays.asList(newString[]{"fun","ban","net"}),everyItem(containsString("n")));
}
//CoreHamcrestMatcherswithassertThat
@Test
publicvoidtestAssertThatHamcrestCoreMatchers(){
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.<Integer>either(equalTo(3)).or(equalTo(4))));
assertThat(newObject(),not(sameInstance(newObject())));
}
@Test
publicvoidtestAssertTrue(){
assertTrue("failure-shouldbetrue",true);
}
}
4.介绍我们的案例
让我们来写一个简单的程序吧。其目的是为漫画提供一个基本的搜索引擎。
4.1.Maven依赖
首先,需要添加一些依赖到我们的工程中。
org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web org.projectlombok lombok 1.16.20 provided
4.2.定义Model
我们的模型非常简单,只有两个类组成:Manga和MangaResult
4.2.1.Manga类
Manga类表示系统检索到的Manga实例。使用Lombok来减少样板代码。
packagecom.mgiglione.model;
importlombok.AllArgsConstructor;
importlombok.Builder;
importlombok.Getter;
importlombok.NoArgsConstructor;
importlombok.Setter;
@Getter@Setter@NoArgsConstructor@AllArgsConstructor@Builder
publicclassManga{
privateStringtitle;
privateStringdescription;
privateIntegervolumes;
privateDoublescore;
}
4.2.2.MangaResult
MangaResult类是包含了一个MangaList的包装类。
packagecom.mgiglione.model;
importjava.util.List;
importlombok.Getter;
importlombok.NoArgsConstructor;
importlombok.Setter;
@Getter@Setter@NoArgsConstructor
publicclassMangaResult{
privateListresult;
}
4.3.实现Service
为实现本Service,我们将使用由JikanMoe提供的免费API接口。
RestTemplate是用来对API进行发起REST调用的Spring类。
packagecom.mgiglione.service;
importjava.util.List;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.stereotype.Service;
importorg.springframework.web.client.RestTemplate;
importcom.mgiglione.model.Manga;
importcom.mgiglione.model.MangaResult;
@Service
publicclassMangaService{
Loggerlogger=LoggerFactory.getLogger(MangaService.class);
privatestaticfinalStringMANGA_SEARCH_URL="http://api.jikan.moe/search/manga/";
@Autowired
RestTemplaterestTemplate;
publicListgetMangasByTitle(Stringtitle){
returnrestTemplate.getForEntity(MANGA_SEARCH_URL+title,MangaResult.class).getBody().getResult();
}
}
4.4.实现Controller
下一步就是写一个暴露了两个端点的RESTController,一个是同步的,一个是异步的,其仅用于测试目的。该Controller使用了上面定义的Service。
packagecom.mgiglione.controller;
importjava.util.List;
importjava.util.concurrent.CompletableFuture;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.scheduling.annotation.Async;
importorg.springframework.web.bind.annotation.PathVariable;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
importorg.springframework.web.bind.annotation.ResponseBody;
importorg.springframework.web.bind.annotation.RestController;
importcom.mgiglione.model.Manga;
importcom.mgiglione.service.MangaService;
@RestController
@RequestMapping(value="/manga")
publicclassMangaController{
Loggerlogger=LoggerFactory.getLogger(MangaController.class);
@Autowired
privateMangaServicemangaService;
@RequestMapping(value="/async/{title}",method=RequestMethod.GET)
@Async
publicCompletableFuture>searchASync(@PathVariable(name="title")Stringtitle){
returnCompletableFuture.completedFuture(mangaService.getMangasByTitle(title));
}
@RequestMapping(value="/sync/{title}",method=RequestMethod.GET)
public@ResponseBody>searchSync(@PathVariable(name="title")Stringtitle){
returnmangaService.getMangasByTitle(title);
}
}
4.5.启动并测试系统
mvnspring-boot:run
然后,Let'stryit:
curlhttp://localhost:8080/manga/async/ken curlhttp://localhost:8080/manga/sync/ken
示例输出:
{
"title":"RurouniKenshin:MeijiKenkakuRomantan",
"description":"TenyearshavepassedsincetheendofBakumatsu,aneraofwarthatsawtheuprisingofcitizensagainsttheTokugawashogunate.Therevolutionarieswantedtocreateatimeofpeace,andathrivingc...",
"volumes":28,
"score":8.69
},
{
"title":"Sun-KenRock",
"description":"ThestoryrevolvesaroundKen,amanfromanupper-classfamilythatwasorphanedyoungduetohisfamily'sinvolvementwiththeYakuza;hebecameahighschooldelinquentknownforfighting.Theonly...",
"volumes":25,
"score":8.12
},
{
"title":"YumekuiKenbun",
"description":"Forthosewhosuffernightmares,helpawaitsattheGinseikanTeaHouse,wherepatronscanordermuchmorethanjustDarjeeling.Hirukoisaspecialkindofaprivateinvestigator.He'sadreameater....",
"volumes":9,
"score":7.97
}
5.SpringBoot应用的单元测试
SpringBoot提供了一个强大的类以使测试变得简单:@SpringBootTest注解
可以在基于SpringBoot运行的测试类上指定此注解。
除常规SpringTestContextFramework之外,其还提供以下功能:
- 当@ContextConfiguration(loader=…)没有特别声明时,使用SpringBootContextLoader作为默认ContextLoader。
- 在未使用嵌套的@Configuration注解,且未显式指定相关类时,自动搜索@SpringBootConfiguration。
- 允许使用Properties来自定义Environment属性。
- 对不同的Web环境模式提供支持,包括启动在已定义或随机端口上的完全运行的Web服务器的功能。
- 注册TestRestTemplate和/或WebTestClientBean,以便在完全运行在Web服务器上的Web测试中使用。
此处,我们仅有两个组件需要测试:MangaService和MangaController
5.1.对MangaService进行单元测试
为了测试MangaService,我们需要将其与外部组件隔离开来。本例中,只需要一个外部组件:RestTemplate,我们用它来调用远程API。
我们需要做的是模拟RestTemplateBean,并让它始终以固定的给定响应进行响应。SpringTest结合并扩展了Mockito库,通过@MockBean注解,我们可以配置模拟Bean。
packagecom.mgiglione.service.test.unit;
importstaticorg.mockito.ArgumentMatchers.any;
importstaticorg.mockito.Mockito.when;
importjava.io.IOException;
importjava.util.List;
importorg.junit.Test;
importorg.junit.runner.RunWith;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.boot.test.context.SpringBootTest;
importorg.springframework.boot.test.mock.mockito.MockBean;
importorg.springframework.http.HttpStatus;
importorg.springframework.http.ResponseEntity;
importorg.springframework.test.context.junit4.SpringRunner;
importorg.springframework.web.client.RestTemplate;
importstaticorg.assertj.core.api.Assertions.assertThat;
importcom.mgiglione.model.Manga;
importcom.mgiglione.model.MangaResult;
importcom.mgiglione.service.MangaService;
importcom.mgiglione.utils.JsonUtils;
@RunWith(SpringRunner.class)
@SpringBootTest
publicclassMangaServiceUnitTest{
@Autowired
privateMangaServicemangaService;
//MockBeanistheannotationprovidedbySpringthatwrapsmockitoone
//AnnotationthatcanbeusedtoaddmockstoaSpringApplicationContext.
//Ifanyexistingsinglebeanofthesametypedefinedinthecontextwillbereplacedbythemock,ifnoexistingbeanisdefinedanewonewillbeadded.
@MockBean
privateRestTemplatetemplate;
@Test
publicvoidtestGetMangasByTitle()throwsIOException{
//Parsingmockfile
MangaResultmRs=JsonUtils.jsonFile2Object("ken.json",MangaResult.class);
//Mockingremoteservice
when(template.getForEntity(any(String.class),any(Class.class))).thenReturn(newResponseEntity(mRs,HttpStatus.OK));
//Isearchforgokubutsystemwillusemockedresponsecontainingonlyken,soIcancheckthatmockisused.
ListmangasByTitle=mangaService.getMangasByTitle("goku");
assertThat(mangasByTitle).isNotNull()
.isNotEmpty()
.allMatch(p->p.getTitle()
.toLowerCase()
.contains("ken"));
}
}
5.2.对MangaController进行单元测试
正如在MangaService的单元测试中所做的那样,我们需要隔离组件。在这种情况下,我们需要模拟MangaServiceBean。
然后,我们还有一个问题……Controller部分是管理HttpRequest的系统的一部分,因此我们需要一个系统来模拟这种行为,而非启动完整的HTTP服务器。
MockMvc是执行该操作的Spring类。其可以以不同的方式进行设置:
- 使用StandaloneContext
- 使用WebApplicationContext
- 让Spring通过在测试类上使用@SpringBootTest、@AutoConfigureMockMvc这些注解来加载所有的上下文,以实现自动装配
- 让Spring通过在测试类上使用@WebMvcTest注解来加载Web层上下文,以实现自动装配
packagecom.mgiglione.service.test.unit;
importstaticorg.hamcrest.Matchers.is;
importstaticorg.mockito.ArgumentMatchers.any;
importstaticorg.mockito.Mockito.when;
importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
importstaticorg.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
importstaticorg.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
importjava.util.ArrayList;
importjava.util.List;
importorg.junit.Before;
importorg.junit.Test;
importorg.junit.runner.RunWith;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.boot.test.context.SpringBootTest;
importorg.springframework.boot.test.mock.mockito.MockBean;
importorg.springframework.http.MediaType;
importorg.springframework.test.context.junit4.SpringRunner;
importorg.springframework.test.web.servlet.MockMvc;
importorg.springframework.test.web.servlet.MvcResult;
importorg.springframework.web.context.WebApplicationContext;
importcom.mgiglione.controller.MangaController;
importcom.mgiglione.model.Manga;
importcom.mgiglione.service.MangaService;
@SpringBootTest
@RunWith(SpringRunner.class)
publicclassMangaControllerUnitTest{
MockMvcmockMvc;
@Autowired
protectedWebApplicationContextwac;
@Autowired
MangaControllermangaController;
@MockBean
MangaServicemangaService;
/**
*Listofsamplesmangas
*/
privateListmangas;
@Before
publicvoidsetup()throwsException{
this.mockMvc=standaloneSetup(this.mangaController).build();//Standalonecontext
//mockMvc=MockMvcBuilders.webAppContextSetup(wac)
//.build();
Mangamanga1=Manga.builder()
.title("Hokutonoken")
.description("Theyearis199X.TheEarthhasbeendevastatedbynuclearwar...")
.build();
Mangamanga2=Manga.builder()
.title("YumekuiKenbun")
.description("Forthosewhosuffernightmares,helpawaitsattheGinseikanTeaHouse,wherepatronscanordermuchmorethanjustDarjeeling.Hirukoisaspecialkindofaprivateinvestigator.He'sadreameater....")
.build();
mangas=newArrayList<>();
mangas.add(manga1);
mangas.add(manga2);
}
@Test
publicvoidtestSearchSync()throwsException{
//Mockingservice
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("Hokutonoken")))
.andExpect(jsonPath("$[1].title",is("YumekuiKenbun")));
}
@Test
publicvoidtestSearchASync()throwsException{
//Mockingservice
when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
MvcResultresult=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("Hokutonoken")));
}
}
正如在代码中所看到的那样,选择第一种解决方案是因为其是最轻量的一个,并且我们可以对Spring上下文中加载的对象有更好的治理。
在异步测试中,必须首先通过调用服务,然后启动asyncDispatch方法来模拟异步行为。
6.SpringBoot应用的集成测试
对于集成测试,我们希望提供下游通信来检查我们的主要组件。
6.1.对MangaService进行集成测试
这个测试也是非常简单的。我们不需要模拟任何东西,因为我们的目的就是要调用远程MangaAPI。
packagecom.mgiglione.service.test.integration;
importstaticorg.assertj.core.api.Assertions.assertThat;
importjava.util.List;
importorg.junit.Test;
importorg.junit.runner.RunWith;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.boot.test.context.SpringBootTest;
importorg.springframework.test.context.junit4.SpringRunner;
importcom.mgiglione.model.Manga;
importcom.mgiglione.service.MangaService;
@RunWith(SpringRunner.class)
@SpringBootTest
publicclassMangaServiceIntegrationTest{
@Autowired
privateMangaServicemangaService;
@Test
publicvoidtestGetMangasByTitle(){
ListmangasByTitle=mangaService.getMangasByTitle("ken");
assertThat(mangasByTitle).isNotNull().isNotEmpty();
}
}
6.2.对MangaController进行集成测试
这个测试和单元测试很是相似,但在这个案例中,我们无需再模拟MangaService。
packagecom.mgiglione.service.test.integration;
importstaticorg.hamcrest.Matchers.hasItem;
importstaticorg.hamcrest.Matchers.is;
importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
importstaticorg.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
importstaticorg.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
importstaticorg.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
importstaticorg.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
importorg.junit.Before;
importorg.junit.Test;
importorg.junit.runner.RunWith;
importorg.springframework.beans.factory.annotation.Autowired;
importorg.springframework.boot.test.context.SpringBootTest;
importorg.springframework.http.MediaType;
importorg.springframework.test.context.junit4.SpringRunner;
importorg.springframework.test.web.servlet.MockMvc;
importorg.springframework.test.web.servlet.MvcResult;
importorg.springframework.web.context.WebApplicationContext;
importcom.mgiglione.controller.MangaController;
@SpringBootTest
@RunWith(SpringRunner.class)
publicclassMangaControllerIntegrationTest{
//@Autowired
MockMvcmockMvc;
@Autowired
protectedWebApplicationContextwac;
@Autowired
MangaControllermangaController;
@Before
publicvoidsetup()throwsException{
this.mockMvc=standaloneSetup(this.mangaController).build();//Standalonecontext
//mockMvc=MockMvcBuilders.webAppContextSetup(wac)
//.build();
}
@Test
publicvoidtestSearchSync()throwsException{
mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.*.title",hasItem(is("HokutonoKen"))));
}
@Test
publicvoidtestSearchASync()throwsException{
MvcResultresult=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("HokutonoKen"))));
}
}
7.结论
我们已经了解了在SpringBoot环境下单元测试和集成测试的主要不同,了解了像Hamcrest这样简化测试编写的框架。当然,也可以在我的GitHub仓库里找到所有代码。
原文:https://dzone.com/articles/unit-and-integration-tests-in-spring-boot-2
作者:MarcoGiglione
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。