spring测试

15 篇文章 0 订阅
模拟测试数据:

Mock测试技术能够避免你为了测试一个方法,却需要自行构建整个依赖关系的工作,并且能够让你专注于当前被测试对象的逻辑,而不是其依赖的其他对象的逻辑

举例来说,比如你需要测试Foo.methodA,而这个方法依赖了Bar.methodB,又传递依赖到了Zoo.methodC,于是它们的依赖关系就是Foo->Bar->Zoo,所以在测试代码里你必须自行new Bar和Zoo

有人会说:"我直接用Spring的DI机制不就行了吗?"

但是:因为Foo方法内部调用了Bar和Zoo的方法,所以你对其做单元测试的时候,必须完全了解Bar和Zoo方法的内部逻辑,并且谨慎的传参和assert结果,一旦Bar和Zoo的代码修改了,你的Foo测试代码很可能就会运行失败

所以这个时候我们需要一种机制,让我们在测试Foo的时候不依赖于Bar和Zoo的具体实现,即不关心其内部逻辑,只关注Foo内部的逻辑,从而将Foo的每个逻辑分支都测试到

你肯定会问,这样的测试有意义吗?在真实环境里Foo用的是真的Bar而不是假的Bar,你用假的Bar测试成功能代表真实环境不出问题?

其实这里就提现了我们分层测试的原则了,假Bar代表的是一个行为正确的Bar,用它来测试就能验证"在Bar行为正确的情况下Foo的行为是否正确",而真Bar的行为是否正确由它自己的测试代码来验证

Mock技术的另一个好处是能够让你尽量避免集成测试,比如我们可以Mock一个Repository(数据库操作类),让我们尽量多写单元测试,提高测试代码执行效率

当Bean存在这种依赖关系当时候:LooImpl -> FooImpl -> Bar,我们应该怎么测试呢?

按照Mock测试的原则,这个时候我们应该mock一个Foo对象,把这个注入到LooImpl对象里

不过如果你不想mock Foo而是想mock Bar的时候,其实做法和前面也差不多,Spring会自动将mock Bar注入到FooImpl中,然后将FooImpl注入到LooImpl中

例如:


@MockBean

private Bar bar;

@Autowired

private Loo loo;

@Test public void testCheckCodeDuplicate() throws Exception { when(bar.getAllCodes()).thenReturn(Collections.singleton("123"));

assertEquals(loo.checkCodeDuplicate("123"), true);

}

得益于Spring Test Framework,我们能够很方便地对依赖关系中任意层级的任意Bean做mock。

隔离组件:

单元测试测试的是某个方法的执行情况是否符合我们的预期,因此我们需要保证的是编写的方法不会因为上下文环境的改变而导致方法的执行情况发生改变
测试service:

我们需要测试一个服务MangaService,为了测试MangaService,我们需要将其与外部组件隔离。在本例子的MangaService中其引入了一个外部组件:RestTemplate,MangaService用它来调用远程API,我们需要做的是模拟RestTemplate bean并让它始终以固定的给定响应进行响应,这样就实现了MangaService与外部组件的隔离

(模拟方法相应需要使用插件mockito)

例如:

@MockBean private RestTemplate template;

@Test//测试方法
public void testGetMangasByTitle() throws IOException {
// 解析模拟文件
MangaResult mRs = JsonUtils.jsonFile2Object("ken.json", MangaResult.class);
// 模拟远程调用方法,也就是模拟RestTemplate 让其使用设置的相应对象来相应对其方法的调用 when(template.getForEntity(ArgumentMatchers.any(String.class), ArgumentMatchers.any(Class.class))).thenReturn(new ResponseEntity(mRs, HttpStatus.OK)); List<Manga> mangasByTitle = mangaService.getMangasByTitle("goku");

//断言测试结果
assertThat(mangasByTitle)
.isNotNull()
.isNotEmpty()
.allMatch(p -> p.getTitle() .toLowerCase() .contains("ken")); }

流程:

我们首先声明了对象template是我们模拟的RestTemplate,然后使用mockito模拟了template的getForEntity方法,使得下面当mangaService调用template的getForEntity方法的时候调用的是模拟的对象,获取的相应是我们模拟的相应

ArgumentMatchers用来进行参数匹配

那何为参数匹配?这里我的理解就是不写一个固定的值,利用一个方法来匹配某一种参数,这里的anyInt()就是一个ArgumentMatcher,指任何一个int值,anyInt()作为类型检查,不可以匹配null,匹配空使用isNull

测试Controller:

正如在服务的单元测试中所做的那样,在测试controller的时候我们同样需要隔离组件。在这种情况下,我们需要模拟MangaService bean

然后,我们还有一个问题,Controller部分是管理HttpRequest的系统的一部分,因此我们需要一个系统来模拟这种行为,而无需启动完整的HTTP服务器。

MockMvc就是这样做的Spring类。可以用不同的方式设置它:

使用 Standalone Context
使用 WebApplication Context
spring通过在测试类上使用@SpringBootTest @AutoConfigureMockMvc注解自动加载所有上下文来自动配置它
让Spring通过在测试类@WebMvcTest上使用这些注释加载Web层上下文来自动配置它
例如:


@Test public void testSearchSync() throws Exception { 
// 模拟service行为
	when(mangaService.getMangasByTitle(any(String.class)))
		.thenReturn(mangas); 	

	mockMvc.perform(get("/manga/sync/ken")
		.contentType(MediaType.APPLICATION_JSON))
		.andExpect(status().isOk())
		.andExpect(jsonPath("$[1].title", is("Yumekui Kenbun")));
}

首先我们在@before注解标注的方法里面初始化了列表mangas对象,然后使用mockito模拟了对MessageService的getManagasByTitle()方法的请求,然后使用mockMvc来模拟httpRequest,然后对Controller返回的数据使用andExpect()方法来判断返回的数据是否是我们期待的结果

对controller的测试:

@Test

public void getTestOrderSns() throws Exception {

String url = "/spiderOrder/gradOrderSource";

mockMvc.perform(MockMvcRequestBuilders.post(url)

.param("date_first", "AA015795")
.param("date_second", "0.00")
.param("date_third", "10.00"))
.andDo(print())
.andExpect(MockMvcResultMatchers.status().isOk());//测试接口返回状态



//.andExpect(MockMvcResultMatchers.content().string("365"));//测试接口返回内容
//.andExpect(MockMvcResultMatchers.cookie().exists("name"));//测试接口返回的cookie
// andDo()添加一个结果处理器
// andExpect()添加断言
// andReturn()返回处理结果
}

对dao层的测试:

在Spring Framework 3的单元测试中,针对数据库单元测试,一般采用@Rollback(true)进行回滚,但是在Spring Boot中,@Rollback已经无法回滚数据,对数据库操作完全无效,经过查阅Spring Boot的单元测试文档,发现数据回滚的注解已改为@Transactional

@Resource//对email的数据库操作
private EmailActionService actionService;

@Test
@Transactional
public void testSave() {

assertThat(actionService, IsNull.notNullValue());
EmailAction action = new EmailAction();
action.setUserId(100L);
action.setEnterTime(new Date());
action.setUsername(10);
actionService.save(action);
assertThat(action.getId(), IsNull.notNullValue());
}

单元测试会有很大的时间成本,而且不同的方法的可测性以及出错的可能都不相同

例如对数据库的增删改查

集成测试:

集成测试实际上就是我们常用的测试,因为集成测试实际上就是在代码层面上看方法相应是否正常

整体开发完成之后进入集成测试,spring boot项目的启动入口在 Application类中,直接运行run方法就可以启动项目,但是在调试的过程中我们肯定需要不断的去调试代码,如果每修改一次代码就需要手动重启一次服务就很麻烦,spring boot非常贴心的给出了热部署的支持,很方便在web项目中调试使用

在idea中查看覆盖率挺简单的,idea中支持三种插件来查看覆盖率,每种插件统计明细各有千秋,分别是idea自带插件、JaCoCo、Emma

打包测试

项目开发完后,我们写了100个测试用例类,我不能每个类都点击进去,然后慢慢执行,SpringBoot提供了打包测试的方式:我们用一个类,把所有的测试类整理进去,然后直接运行这个类,所有的测试类都会执行

测试成本:

TestContext Framework提供了ApplicationContext的一致加载和上下文的缓存。支持缓存加载的上下文很重要,因为启动时间可能成为一个问题 - 不是因为Spring本身的开销,而是因为Spring容器实例化的对象需要时间来实例化。例如,具有50到100个Hibernate映射文件的项目可能需要10到20秒来加载映射文件,并且在每个测试夹具中运行每个测试之前产生该成本会导致整体测试运行速度变慢,从而降低生产率

默认情况下,一旦加载,配置 ApplicationContext就会重复用于每个测试。因此,设置成本仅发生一次(每个测试夹具),或者说每个测试类中测试方法一起执行的时候,设置成本只会发生一次

测试影响:

访问真实数据库的测试中的一个常见问题是它们对持久性存储的状态的影响,即使您使用的是开发数据库,​​对状态的更改也可能会影响将来的测试。此外,许多操作(例如插入或修改持久数据)无法在事务外执行(或验证)

TestContext框架解决了这个问题。默认情况下,框架将为每个测试创建并回滚事务。您只需编写可以假定存在事务的代码

事务前后执行某些方法

有时,您需要在事务测试方法之前或之后但在事务上下文之外执行某些代码,例如,在执行测试之前验证初始数据库状态或在测试执行后验证预期的事务提交行为(例如,如果测试配置为不回滚事务)。 TransactionalTestExecutionListener支持 @BeforeTransaction和@AfterTransaction注释正是为了这样的场景。只需使用public void 其中一个注释注释测试类中的任何方法,并TransactionalTestExecutionListener确保在适当的时间执行事务前方法或事务方法之后。

在spring中如何加快测试速度?

spring中测试速度慢主要是由于每次运行测试方法都需要启动spring,spring的启动是一个耗时操作,那么有没有一种方式可以不启动spring,直接把测试方法当做一个main方法运行呢?

1.测试类需要使用@RunWith(MockitoJUnitRunner.class)标识,标识此测试类是一个mock的测试类

2.被测试的类需要时实现类而不能是接口,并使用注解@InjectMocks标识

3.被测试类中依赖的类需要使用@Mock标识

4.使用when方法来mock当依赖的类被调用的时候返回的值

这样当我们在测试方法俩里面调用被测试类的方法的时候,不需要启动spring就可以直接运行测试

例如:

@RunWith(MockitoJUnitRunner.class)

public class SpiderImgTest {

@InjectMocks

private HcbImgParseServiceImpl hcbImgParseService;

@Mock

private SpiderOrderSourceService spiderOrderSourceService;

@Before

public void init(){

when(spiderOrderSourceService.selectOrderSourceByTypeAndId(anyString(),anyInt())).thenReturn(null);

}

@Test

public void parseHcbImgInfo(){

String imgInfo = "[{\"words\":\"信息详情及发布者资料\"}]";

boolean isParse = hcbImgParseService.parseHcbImgInfo(imgInfo);

System.out.print(isParse);

}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值