如何进行单元测试
大话单测代码写完了是不是就万事大吉只差上线了我不知道大家有没有写单测的习惯有的公司要求写有的公司根本就不要求写。写单元测试确实是一个体力活一点都不比写代码轻松所以很多程序员比较排斥单元测试更不用说集成测试了。如果不写单元测试那代码的质量如何保证呢咱不是有QA吗靠QA呗出了事儿算QA的但研发一定是幸免于难的必定这个事儿是你干的至少有你一半的责任。单元测试是质量的第一道把关至关重要.那什么样的单元测试是好的呢干一个事儿肯定得有衡量的方法我给你一一介绍。环境搭建单元测试还需要搭建环境吗?我们的脚手架不都已经搭建好了吗如何搭建Spring Boot脚手架 - 掘金 (juejin.cn)但我想给你介绍一下另外一种好玩的方式。在目前分布式的体系架构下我们是不是要依赖很多中间件 比如Redis、DB、Kafka等如果做单元测试的话依赖的中间件怎么办呢使用dev环境的中间件不就可以了吗但会遇到一个问题是dev环境大家共用万一不小心破坏了你的数据不就game over了吗总不能不让别人用吧。除了环境的问题还需要考虑每个case之间的数据是隔离的这样才能保证case的准确性。什么叫准确性呢这个case执行1次和执行100次的结果是一样的不会因着其他因素而改变这也是科学中的可重复性。在介绍脚手架的文章中我们提到了内存版的Redis、kafka、Db, 接下来我们来看看怎么使用的直接上代码导入pomdependency groupIdit.ozimov/groupId artifactIdembedded-redis/artifactId version0.7.2/version scopetest/scope /dependency dependency groupIdch.vorburger.mariaDB4j/groupId artifactIdmariaDB4j/artifactId version2.4.0/version scopetest/scope /dependency dependency groupIdorg.mariadb.jdbc/groupId artifactIdmariadb-java-client/artifactId version2.5.2/version scopetest/scope /dependency dependency artifactIdjunit-platform-launcher/artifactId groupIdorg.junit.platform/groupId scopetest/scope /dependency dependency artifactIdjunit-vintage-engine/artifactId groupIdorg.junit.vintage/groupId version5.9.0/verison /dependencyApplicationTests 测试启动类 该类主要职责是启动redis ServerDbServer.SpringBootTest(classes {LifeCycleManagement.class, KafkaTemplateConfig.class}) ActiveProfiles({unit}) Slf4j DirtiesContext(classMode DirtiesContext.ClassMode.AFTER_CLASS) public abstract class ApplicationTests { public static RedisServer redisServer; BeforeAll public static void beforeAll() throws ManagedProcessException { log.info(mariadb start); LifeCycleManagement.initDB(); log.info(redis server start); redisServer RedisServer.builder().setting(maxmemory 200m).port(8697).build(); redisServer.start(); } AfterAll public static void afterAll() throws ManagedProcessException { log.info(mariadb close); LifeCycleManagement.closeDB(); log.info(redis server stop); redisServer.stop(); } }LifeCycleManagement 该类职责是初始化DB库启动kafka ServerTestConfiguration Slf4j public class LifeCycleManagement { private static final int NUMBER_OF_BROKERS 1; public static DB db; Value(${spring.cloud.sentinel.enabled}) private static boolean sentinel; public static void initDB() throws ManagedProcessException { DBConfigurationBuilder configBuilder DBConfigurationBuilder.newBuilder(); configBuilder.setPort(3308); // OR, default: setPort(0); autom. detect free port configBuilder.setDataDir(./data); // just an example configBuilder.addArg( --userroot); db DB.newEmbeddedDB(configBuilder.build()); db.start(); db.createDB(test); db.source(script/source_table_str.sql, test); } public static void closeDB() throws ManagedProcessException { if (db ! null) { db.stop(); } } public static int[] setupPorts() { return new int[NUMBER_OF_BROKERS]; } Bean public EmbeddedKafkaBroker initKafka() { log.info(kafka server start); boolean CONTROLLER_SHUTDOWN true; int NUMBER_OF_PARTITIONS 1; EmbeddedKafkaBroker embeddedKafkaBroker new EmbeddedKafkaBroker(NUMBER_OF_BROKERS, CONTROLLER_SHUTDOWN, NUMBER_OF_PARTITIONS, new String[]{}) .kafkaPorts(setupPorts()).zkPort(0) .zkConnectionTimeout(EmbeddedKafkaBroker.DEFAULT_ZK_CONNECTION_TIMEOUT) .zkSessionTimeout(EmbeddedKafkaBroker.DEFAULT_ZK_SESSION_TIMEOUT); Properties properties new Properties(); properties.put(listeners, PLAINTEXT://127.0.0.1:9091); properties.put(port, 9091); properties.put(auto.create.topics.enable, true); embeddedKafkaBroker.brokerProperties((MapString, String) (Map?, ?) properties); return embeddedKafkaBroker; } }BaseTest 该类主要初始化mockMvcpublic abstract class BaseTest extends ApplicationTests { public static MockMvc mockMvc; Autowired WebApplicationContext webApplicationContext; AfterEach void afterEach() { } BeforeEach public void beforeEach() throws ManagedProcessException { MiddleWareLifeCycleManagement.db.source(script/clean.sql, test); // 在这初始mock所有的过滤器都不会加载 mockMvc MockMvcBuilders.webAppContextSetup(webApplicationContext) .addFilter(webApplicationContext.getBean(ContentCachingTestFilter.class)).build(); } protected ResultActions doAction(String content, String uri) throws Exception { return mockMvc .perform(MockMvcRequestBuilders.post(uri).contentType(MediaType.APPLICATION_JSON) .header(requestTime, System.nanoTime()).content(content)); } protected ResultActions doAction(MultiValueMapString, String map, String uri) throws Exception { return mockMvc.perform(MockMvcRequestBuilders.post(uri).params(map).header(bizId, UUID.randomUUID().toString().replace(-, ))); } }在你的工程中导入以下几个类基本上就可以work了单元测试如何写直接看案例Test void should_return_expired_when_status_isCorrect() throws Exception { ReceiveAwardRequest request givenStatusExpired(); //given assertResponseCodeEquals(request,ResultCode.EXPIRED); //when and then }单元测试基本上遵循这样的结构体given : 封装请求参数when执行请求thenassert断言验证方法名如何进行命名呢这两种方式我在项目中都用了但我更喜欢第二种方式。givenXXX_whenXXX_thenXXX, 从方法命名上一目了然should_XXX_when_XXX说完了结构接下来我给大家介绍一下不同类型的单元测试该如何写1. 断言http接口返回值spring的MockMvc可以帮我发起http 请求get请求mockMvc .perform(MockMvcRequestBuilders.get(uri).queryParams(map));post请求mockMvc.perform(MockMvcRequestBuilders.post(/api/v1/receive) .contentType(MediaType.APPLICATION_JSON) .header(requestTime, System.nanoTime()) .content(content)) .andExpect(MockMvcResultMatchers.jsonPath($.code).value(resultCode.getCode()));如果你想mock第三方服务的返回值可以这么做Mockito.doReturn(false).when(xxx).xxxFunction(Mockito.any());有时候我们需要mock spring中的bean该bean仍然需要spring去管理而不是mock可以这样做SpyBean protected xxxService xxservice 或者 Mockito.spy(bean)2. 断言数据库中的某个值在有些情况下我们验证该case的是否成功的条件是验证数据库中的某个值是不是改变了比如验证一下奖券状态修改从a-b判断该case是否成功就需要看数据库的值是否是bAwaitility.await().pollDelay(100, TimeUnit.MILLISECONDS) .pollInterval(Duration.ofMillis(500)) .until(() - eventService.lambdaQuery() .eq(MsgEvent::getRequestId, 112212) .eq(MsgEvent::getState, MsgEventState.FAILED.getCode()).exists());3. 断言某个方法是否被执行到Mockito.verify(xxxx, Mockito.never()).xxxFunction(Mockito.any());4. 直接mock某个对象Mockito.mock(Clazz class)除此之外还可以断言某个日志关键字是否被打印等等如何执行单元测试执行一个单元测试我们都知道直接在单个case上右键点击run就行如果要执行整个项目的单元测试该如何做呢配置完成之后直接点击run。执行单元测试必须要依赖于IDE吗当然不是了通过命令的方式也可以。单测覆盖率单元测试写完了如何衡量单测写的好不好呢单测覆盖率我们一般使用分支覆盖率来衡量什么是分支覆盖率呢你理解成对if else的覆盖情况如果只覆盖到了if那覆盖率就是50%单测覆盖率如何执行呢1、在pom.xml中添加jacoco插件plugin groupIdorg.jacoco/groupId artifactIdjacoco-maven-plugin/artifactId version0.8.6/version executions execution goals goalprepare-agent/goal /goals /execution execution idreport/id phasetest/phase goals goalreport/goal /goals /execution /executions /plugin2、执行命令mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent test surefire:test surefire-report:report -Dsurefire.reportsDirectory./target/site --settingsD:/IdeaProjects/settings.xml3、结果每个包的分支覆盖率指令覆盖率都赫然显示出来点击包就能看到某个类的覆盖率以及代码执行情况总结单元测试需要花很长时间写导致很多程序员不想写也不愿意写但单元测试的收益是非常大的在我们下次修改代码的时候通过执行单元测试对软件进行第一道把关。除此之外我们做重构也会比较放心真的是 write once run any time。最后下方这份完整的软件测试 视频教程已经整理上传完成需要的朋友们可以自行领取【保证100%免费】软件测试面试文档我们学习必然是为了找到高薪的工作下面这些面试题是来自阿里、腾讯、字节等一线互联网大厂最新的面试资料并且有字节大佬给出了权威的解答刷完这一套面试资料相信大家都能找到满意的工作。