lsekfe 发表于 2022-2-22 10:04:42

你眼中和我眼中的单元测试,看看有何区别?

 大家好,我是Jensen,今天给大家分享一篇单元测试。单元测试,大家都耳熟能详,但在开发同学中,真正掌握单元测试、愿意写单元测试的并不多!或者也可以说,项目压力大,根本没有时间写单元测试。项目压力大,写单元测试就真的浪费时间吗?
  本文以几个简单的测试例子,给大家讲解下我眼中的单元测试。
  万能的main方法
  假设你接了个需求:商品保存时,产品说需要判断商品名称是否可以为空,假设大家不知道开源框架中的方法,那我们自己来实现一遍。
// 我的实现
      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
      }搞定,收工。
  第二天,产品说:产品名称不能是空字符串。
  好吧,立马修改。
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
      }第五天,相安无事。
  通过上面这个例子,我思考了一下:
  ·产品给的需求很模糊,没有详细说明,在联调测试过程中,才发现问题;
  · 开发同学的main方法测试没有覆盖到所有场景,这样就造成了不知道找产品确认需求;
  · 代码中的main方法删除了吗???
  junit牛刀小试
  我们用上面的栗子,再来写个的单元测试。大部分同学的单元测试是这样写的:
   @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
      }我的单元测试是这样写的:
  1. 必须要有断言
  2. 必须要覆盖已知的场景
@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
  案例:Junit 时间&异常测试
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 值得拥有
  注意Mockito读“摸key头”,不是周杰伦唱的那首。
  在 springBoot 项目中,service 方法测试依赖的东西比较多,数据库、缓存、MQ等等,只有项目能够正常启动,单元测试才能启动。
  单元测试启动后,又容易造成脏数据、需要不停地造数据等等。一个复杂的 service 方法,里面有比较多的过程,有数据库操作,有接口调用,大家不理解测试,直接对整个方法进行测试。其实,真正需要测试的内容无非就是这些:
  1. 核心算法
  2. 复杂运算逻辑(如:金额计算、优惠券使用逻辑)
  3. 有幂等需求等
  复杂方法的测试,可以拆解成几个小逻辑的测试,这样依然能够达到测试的效果,提高测试、联调的通过率,并且在发现问题时,能够快速回归。
  个人经验:编写单元测试的时间 + 依靠单元测试解决问题的时间 < 联调发现错误 + 解决问题的时间。
  上面说了这么多,和 mockito 又有什么关系呢?
  如果不懂得拆分方法,掌握哪些方法需要测试,哪些方法不需要测试,那就会造成浪费;
  如果你只会使用junit,那么一些复杂的测试,你每次启动测试时,都需要花大量的时间等待测试启动;如果你掌握了1,想快速进行测试,那么,mockito 就是你最好的选择。
  使用 mockito 进行复杂业务测试
  以下是我们公司的网关功能权限 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<UcRightsEntity> rightList;
      private List<Long> roleIds;
      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);
      }
  }拔高篇——Spring Boot TestSptring Boot Test
  集成了 junit mockito 之大成,还有其他额外的能力。
  spring-boot-starter-test 包含了下面组件:

总结一下
  继续讲下去就炒冷饭了,梳理这篇文章,主要想让告诉大家真正的单元测试应该怎么做。说实话,我自己也只掌握了单元测试的30%而已,但仅仅靠这30%知识,就能:
  ·提高我的代码质量
  · 减少我测试联调时间
  · 让我能够在出现问题时快速定位问题
  因此,我希望大家也可以重视单元测试,学习更多的单元测试知识。一起共勉!









喵喵怪 发表于 2022-2-23 21:05:14

wow
页: [1]
查看完整版本: 你眼中和我眼中的单元测试,看看有何区别?