文转载自微信公众号「架构师修行录」,眼中有何作者Jensen 。和眼转载本文请联系架构师修行录公众号。单元 大家好,测试我是区别Jensen,今天给大家分享一篇单元测试。眼中有何单元测试,和眼大家都耳熟能详,单元但在开发同学中,测试真正掌握单元测试、区别愿意写单元测试的眼中有何并不多!或者也可以说,项目压力大,和眼根本没有时间写单元测试。单元项目压力大,测试写单元测试就真的区别浪费时间吗? 本文以几个简单的测试例子,给大家讲解下我眼中的单元测试。 假设你接了个需求:商品保存时,产品说需要判断商品名称是否可以为空,假设大家不知道开源框架中的方法,那我们自己来实现一遍。 // 我的实现 public static boolean isEmpty(String str) { return str == null; } 写完不放心,我还是写个单元测试测试下 String str = null; System.out.println("null isEmpty: " + isEmpty1(str)); // 结果:true str = "java某花宝典"; System.out.println("非空字符串 isEmpty: " + isEmpty1(str)); // 结果:false } 搞定,收工。 第二天,云南idc服务商产品说:产品名称不能是空字符串。 好吧,立马修改。 public static boolean isEmpty(String str) { return str == null || str.length() == 0;; } public static void main(String[] args) { String str = null; System.out.println("null isEmpty: " + isEmpty2(str)); // 结果:true str = "java某花宝典"; System.out.println("非空字符串 isEmpty: " + isEmpty2(str)); // 结果:false str = ""; System.out.println("空字符串 isEmpty: " + isEmpty2(str)); // 结果:true } 第三天,产品说:空格也不行。 我改还不行吗…… public static boolean isEmpty(String str) { return str == null || str.length() == 0 || Objects.equals(" ", str); } public static void main(String[] args) { String str = null; System.out.println("null isEmpty: " + isEmpty3(str)); // 结果:true str = "java某花宝典"; System.out.println("非空字符串 isEmpty: " + isEmpty3(str)); // 结果:false str = ""; System.out.println("空字符串 isEmpty: " + isEmpty3(str)); // 结果:true str = " "; System.out.println("空格 isEmpty: " + isEmpty3(str)); // 结果:true } 终于做完了。 第四天,产品说:连续空格也不行的。 public static boolean isEmpty(String str) { int strLen; if (cs == null || (strLen = cs.length()) == 0) { return true; } for (int i = 0; i < strLen; i++) { if (!Character.isWhitespace(cs.charAt(i))) { return false; } } return true; } public static void main(String[] args) { String str = null; System.out.println("null isEmpty: " + isEmpty4(str)); // 结果:true str = "java某花宝典"; System.out.println("非空字符串 isEmpty: " + isEmpty4(str)); // 结果:false str = ""; System.out.println("空字符串 isEmpty: " + isEmpty4(str)); // 结果:true str = " "; System.out.println("空格 isEmpty: " + isEmpty4(str)); // 结果:true str = " "; System.out.println("连续空格 isEmpty: " + isEmpty4(str)); // 结果:true } 第五天,相安无事。 通过上面这个例子,我思考了一下: 我们用上面的栗子,再来写个的单元测试。大部分同学的单元测试是这样写的: @Test public void test() { boolean result = MyStringUtils.isEmpty(null); log.info("result: { }", result); // 结果:true result = MyStringUtils.isEmpty("java某花宝典"); log.info("result: { }", result); // 结果:false result = MyStringUtils.isEmpty(""); log.info("result: { }", result); // 结果:true result = MyStringUtils.isEmpty(" "); log.info("result: { }", result); // 结果:true result = MyStringUtils.isEmpty(" "); log.info("result: { }", result); // 结果:true } 必须要有断言必须要覆盖已知的场景@Test public void test2() { boolean result = MyStringUtils.isEmpty(null); log.info("result: { }", result); // 结果:true Assert.assertTrue(result); result = MyStringUtils.isEmpty("java某花宝典"); log.info("result: { }", result); // 结果:false Assert.assertFalse(result); result = MyStringUtils.isEmpty(""); log.info("result: { }", result); // 结果:true Assert.assertTrue(result); result = MyStringUtils.isEmpty(" "); log.info("result: { }", result); // 结果:true Assert.assertTrue(result); result = MyStringUtils.isEmpty(" "); log.info("result: { }", result); // 结果:true Assert.assertTrue(result); } 关于Junit的介绍,网站模板网上太多了,我就不废话了,不是本文的重点大家自己搜下看看就好了。 比如这篇,使用JUnit进行单元测试:https://www.jianshu.com/p/a3fa5d208c93 package com.xxx.test.junit; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @Slf4j public class MyUtilsTest { @Before public void before() { log.info("before##################创建对象"); log.info("每个方法都执行##################每个方法都执行"); } @After public void after() { log.info("after###################销毁对象"); log.info("每个方法都执行##################每个方法都执行"); } @BeforeClass public static void beforeClass() { log.warn("beforeClass##################创建对象"); log.warn("只执行一次##################只执行一次"); } @AfterClass public static void afterClass() { log.warn("afterClass###################销毁对象"); log.warn("只执行一次##################只执行一次"); } @Test public void testIsEmpty() { boolean result = MyUtils.isEmpty(null); log.info("result: { }", result); // 结果:true result = MyUtils.isEmpty("java某花宝典"); log.info("result: { }", result); // 结果:false result = MyUtils.isEmpty(""); log.info("result: { }", result); // 结果:true result = MyUtils.isEmpty(" "); log.info("result: { }", result); // 结果:true result = MyUtils.isEmpty(" "); log.info("result: { }", result); // 结果:true } /** 时间测试 **/ @Test(timeout = 2000) public void testPayTime() { MyUtils.payTime(1000); log.warn("payTime1:1000"); MyUtils.payTime(1000); log.warn("payTime2:1000"); MyUtils.payTime(3000); log.warn("payTime3:3000"); } @Test(timeout = 2000) public void testPayTime2() { MyUtils.payTime(1900); log.warn("payTime1:1900"); } @Test public void testThrowErr2() { log.warn("测试不通过"); MyUtils.throwErr(); } /** 异常测试 **/ @Test(expected = ArithmeticException.class) public void testThrowErr() { log.warn("测试通过"); log.warn("start~~~~~~~~~~~~~~~"); MyUtils.throwErr(); } } 注意Mockito读“摸key头”,不是周杰伦唱的那首。 在 springBoot 项目中,service 方法测试依赖的东西比较多,数据库、缓存、MQ等等,只有项目能够正常启动,单元测试才能启动。 单元测试启动后,又容易造成脏数据、需要不停地造数据等等。一个复杂的 service 方法,里面有比较多的过程,有数据库操作,有接口调用,大家不理解测试,直接对整个方法进行测试。其实,真正需要测试的内容无非就是这些: 复杂方法的测试,可以拆解成几个小逻辑的测试,这样依然能够达到测试的效果,提高测试、联调的通过率,并且在发现问题时,能够快速回归。 个人经验:编写单元测试的时间 + 依靠单元测试解决问题的时间 < 联调发现错误 + 解决问题的时间。 上面说了这么多,和 mockito 又有什么关系呢? Mockito 简明教程: https://www.cnblogs.com/bodhitree/p/9456515.html 以下是我们公司的网关功能权限 mockito 测试案例。 私有静态方法:判断用户是否有权限 private boolean isUserRight(String service, String path, String requestMethod, CacheGatewayUseDTO gatewayUse) { if (isDirectUserRight(gatewayUse.getUserRights(), service, path, requestMethod)) { return true; } return isUserRoleRight(service, path, requestMethod, gatewayUse.getRoleIds()); } 使用 mockito 进行测试 @Slf4j // 使用PowerMockRunner 进行测试 @RunWith(PowerMockRunner.class) // 测试用使用了静态工具类 CacheUtils @PrepareForTest(CacheUtils.class) public class PowerFilterTest { // 测试的类 @InjectMocks private PowerFilter powerFilter; private String service; private String path; private String requestMethod; private CacheGatewayUseDTO gatewayUse; private boolean isMatch; private UcRoleEntity role; private List private List private WhiteListResponse whiteListResponse; @Test public void isUserRight() throws InvocationTargetException, IllegalAccessException { PowerMockito.mockStatic(CacheUtils.class); PowerMockito.when(CacheUtils.notExistNewRole(ArgumentMatchers.anyLong())).thenReturn(false); // administrator uc 角色 role = UcRoleEntity.builder().id(13L).roleCode("RO210111000023").build(); // 创建测试数据 rightList = Arrays.asList( createRight("uc", "/get/**", "GET"), createRight("uc", "/getUserInfo", "GET"), createRight("uc", "/getDept", "GET"), createRight("uc", "/regrister", "POST"), createRight("oms", "/getDept", "GET"), createRight("oms", "/createOrder", "POST"), createRight("oms", "/getOrder", "GET") ); PowerMockito.when(CacheUtils.getNewRole(ArgumentMatchers.anyLong())).thenReturn(BizConverUtils.getCacheRole(role, rightList)); Method method = PowerMockito.method(PowerFilter.class, "isUserRight", String.class, String.class, String.class, CacheGatewayUseDTO.class); service = "uc"; path = "/getUserInfo4"; requestMethod = "GET"; // 无权限 gatewayUse = CacheGatewayUseDTO.builder() .userCode("") .nickname("") .roleIds(Arrays.asList(role.getId())) .userRights(Arrays.asList( UserRightsDO.builder().rightUrl("/getRoleList").requestMethod("GET").services(service).build() , UserRightsDO.builder().rightUrl("/getTest").requestMethod("GET").services(service).build() , UserRightsDO.builder().rightUrl("/getRights").requestMethod("GET").services(service).build() )) .build(); isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse); log.info("isMatch { }", isMatch); Assert.assertTrue(!isMatch); // 直接权限 path = "/getRoleList"; isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse); log.info("isMatch { }", isMatch); Assert.assertTrue(isMatch); // 角色权限,服务不一样,没有权限 path = "/getOrder"; isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse); log.info("isMatch { }", isMatch); Assert.assertTrue(!isMatch); } } 集成了 junit mockito 之大成,还有其他额外的能力。 spring-boot-starter-test 包含了下面组件: 名称 功能 JUnit Java语言的单元测试框架,默认依赖版本是4.12(JUnit5和JUnit4差别比较大,集成方式有不同) Spring Test & Spring Boot Test Spring的测试支持 AssertJ 提供了流式的断言方式 Hamcrest 提供了丰富的 matcher 和 assertThat 结合使用,如:判断bean是否包含某个属性等 Mockito mock框架,可以按类型创建mock对象,可以根据方法参数指定特定的响应,也支持对于mock调用过程的断言 JSONassert 为JSON提供了断言功能 JsonPath 为JSON提供了XPATH功能,类似 JQuery 选择器 分享两篇写得比较好的文章给大家: Spring Boot Test 快速入门: http://ypk1226.com/2018/11/17/spring-boot/spring-boot-test-1/ Spring Boot Test 注解详解: http://ypk1226.com/2018/11/20/spring-boot/spring-boot-test-2/ @SpringBootTest 注解参数 webEnvironment 指定web环境,可选值有:MOCK、RANDOM_PORT、DEFINED_PORT、NONE 名称 功能 MOCK 此值为默认值,该类型提供一个mock环境,此时内嵌的服务(servlet容器)并没有真正启动,也不会监听web端口。 RANDOM_PORT 启动一个真实的web服务,监听一个随机端口。 DEFINED_PORT 启动一个真实的web服务,监听一个定义好的端口(从配置中读取)。 NONE 启动一个非web的ApplicationContext,既不提供mock环境,也不提供真实的web服务。可以理解为,不启动web容器 继续讲下去就炒冷饭了,梳理这篇文章,主要想让告诉大家真正的单元测试应该怎么做。说实话,我自己也只掌握了单元测试的30%而已,但仅仅靠这30%知识,就能: 因此,我希望大家也可以重视单元测试,学习更多的单元测试知识。一起共勉!0x1万能的main方法
0x2junit牛刀小试
我的单元测试是这样写的:
案例:Junit 时间&异常测试
0x3Mockito 值得拥有
0x4使用 mockito 进行复杂业务测试
0x5拔高篇——Spring Boot TestSptring Boot Test
0x6总结一下