51Testing软件测试论坛

 找回密码
 (注-册)加入51Testing

QQ登录

只需一步,快速开始

微信登录,快人一步

查看: 1226|回复: 1
打印 上一主题 下一主题

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

[复制链接]
  • TA的每日心情
    无聊
    前天 09:06
  • 签到天数: 941 天

    连续签到: 3 天

    [LV.10]测试总司令

    跳转到指定楼层
    1#
    发表于 2022-2-22 10:04:42 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
     大家好,我是Jensen,今天给大家分享一篇单元测试。单元测试,大家都耳熟能详,但在开发同学中,真正掌握单元测试、愿意写单元测试的并不多!或者也可以说,项目压力大,根本没有时间写单元测试。项目压力大,写单元测试就真的浪费时间吗?
      本文以几个简单的测试例子,给大家讲解下我眼中的单元测试。
      万能的main方法
      假设你接了个需求:商品保存时,产品说需要判断商品名称是否可以为空,假设大家不知道开源框架中的方法,那我们自己来实现一遍。
    1. // 我的实现
    2.       public static boolean isEmpty(String str) {
    3.           return str == null;
    4.       }
    复制代码
    写完不放心,我还是写个单元测试测试下

    1.    String str = null;
    2.           System.out.println("null isEmpty: " + isEmpty1(str));  // 结果:true
    3.           str = "java某花宝典";
    4.           System.out.println("非空字符串 isEmpty: " + isEmpty1(str));  // 结果:false
    5.       }
    复制代码
    搞定,收工。
      第二天,产品说:产品名称不能是空字符串。
      好吧,立马修改。
    1. public static boolean isEmpty(String str) {
    2.           return str == null || str.length() == 0;;
    3.       }
    4.       public static void main(String[] args) {
    5.           String str = null;
    6.           System.out.println("null isEmpty: " + isEmpty2(str));  // 结果:true
    7.           str = "java某花宝典";
    8.           System.out.println("非空字符串 isEmpty: " + isEmpty2(str));  // 结果:false
    9.           str = "";
    10.           System.out.println("空字符串 isEmpty: " + isEmpty2(str)); // 结果:true
    11.       }
    复制代码
    第三天,产品说:空格也不行。
      我改还不行吗……
    1. public static boolean isEmpty(String str) {
    2.           return str == null || str.length() == 0  || Objects.equals(" ", str);
    3.       }
    4.       public static void main(String[] args) {
    5.           String str = null;
    6.           System.out.println("null isEmpty: " + isEmpty3(str));  // 结果:true
    7.           str = "java某花宝典";
    8.           System.out.println("非空字符串 isEmpty: " + isEmpty3(str));  // 结果:false
    9.           str = "";
    10.           System.out.println("空字符串 isEmpty: " + isEmpty3(str)); // 结果:true
    11.           str = " ";
    12.           System.out.println("空格 isEmpty: " + isEmpty3(str)); // 结果:true
    13.       }
    复制代码
    终于做完了。
      第四天,产品说:连续空格也不行的。
    1.    public static boolean isEmpty(String str) {
    2.           int strLen;
    3.           if (cs == null || (strLen = cs.length()) == 0) {
    4.               return true;
    5.           }
    6.           for (int i = 0; i < strLen; i++) {
    7.               if (!Character.isWhitespace(cs.charAt(i))) {
    8.                   return false;
    9.               }
    10.           }
    11.           return true;
    12.       }
    13.       public static void main(String[] args) {
    14.           String str = null;
    15.           System.out.println("null isEmpty: " + isEmpty4(str));  // 结果:true
    16.           str = "java某花宝典";
    17.           System.out.println("非空字符串 isEmpty: " + isEmpty4(str));  // 结果:false
    18.           str = "";
    19.           System.out.println("空字符串 isEmpty: " + isEmpty4(str)); // 结果:true
    20.           str = " ";
    21.           System.out.println("空格 isEmpty: " + isEmpty4(str)); // 结果:true
    22.           str = "       ";
    23.           System.out.println("连续空格 isEmpty: " + isEmpty4(str)); // 结果:true
    24.       }
    复制代码
    第五天,相安无事。
      通过上面这个例子,我思考了一下:
      ·产品给的需求很模糊,没有详细说明,在联调测试过程中,才发现问题;
      · 开发同学的main方法测试没有覆盖到所有场景,这样就造成了不知道找产品确认需求;
      · 代码中的main方法删除了吗???
      junit牛刀小试
      我们用上面的栗子,再来写个的单元测试。大部分同学的单元测试是这样写的:
    1.    @Test
    2.       public void test() {
    3.           boolean result = MyStringUtils.isEmpty(null);
    4.           log.info("result: {}", result); // 结果:true
    5.           result = MyStringUtils.isEmpty("java某花宝典");
    6.           log.info("result: {}", result); // 结果:false
    7.           result = MyStringUtils.isEmpty("");
    8.           log.info("result: {}", result); // 结果:true
    9.           result = MyStringUtils.isEmpty(" ");
    10.           log.info("result: {}", result); // 结果:true
    11.           result = MyStringUtils.isEmpty("     ");
    12.           log.info("result: {}", result); // 结果:true
    13.       }
    复制代码
    我的单元测试是这样写的:
      1. 必须要有断言
      2. 必须要覆盖已知的场景
    1. @Test
    2.       public void test2() {
    3.           boolean result = MyStringUtils.isEmpty(null);
    4.           log.info("result: {}", result); // 结果:true
    5.           Assert.assertTrue(result);
    6.           
    7.           result = MyStringUtils.isEmpty("java某花宝典");
    8.           log.info("result: {}", result); // 结果:false
    9.           Assert.assertFalse(result);
    10.           
    11.           result = MyStringUtils.isEmpty("");
    12.           log.info("result: {}", result); // 结果:true
    13.           Assert.assertTrue(result);
    14.           
    15.           result = MyStringUtils.isEmpty(" ");
    16.           log.info("result: {}", result); // 结果:true
    17.           Assert.assertTrue(result);
    18.           
    19.           result = MyStringUtils.isEmpty("     ");
    20.           log.info("result: {}", result); // 结果:true
    21.           Assert.assertTrue(result);
    22.       }
    复制代码
    关于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 测试案例。
      私有静态方法:判断用户是否有权限
    1. private boolean isUserRight(String service, String path, String requestMethod, CacheGatewayUseDTO gatewayUse) {
    2.           if (isDirectUserRight(gatewayUse.getUserRights(), service, path, requestMethod)) {
    3.               return true;
    4.           }
    5.           return isUserRoleRight(service, path, requestMethod, gatewayUse.getRoleIds());
    6.       }
    复制代码
     使用 mockito 进行测试

    1.  @Slf4j
    2.   // 使用PowerMockRunner 进行测试
    3.   @RunWith(PowerMockRunner.class)
    4.   // 测试用使用了静态工具类 CacheUtils
    5.   @PrepareForTest(CacheUtils.class)
    6.   public class PowerFilterTest {
    7.       // 测试的类
    8.       @InjectMocks
    9.       private PowerFilter powerFilter;
    10.      
    11.       private String service;
    12.       private String path;
    13.       private String requestMethod;
    14.       private CacheGatewayUseDTO gatewayUse;
    15.       private boolean isMatch;
    16.       private UcRoleEntity role;
    17.       private List<UcRightsEntity> rightList;
    18.       private List<Long> roleIds;
    19.       private WhiteListResponse whiteListResponse;
    20.       @Test
    21.       public void isUserRight() throws InvocationTargetException, IllegalAccessException {
    22.           PowerMockito.mockStatic(CacheUtils.class);
    23.           PowerMockito.when(CacheUtils.notExistNewRole(ArgumentMatchers.anyLong())).thenReturn(false);
    24.           // administrator uc 角色
    25.           role = UcRoleEntity.builder().id(13L).roleCode("RO210111000023").build();
    26.           // 创建测试数据
    27.           rightList = Arrays.asList(
    28.                   createRight("uc", "/get/**", "GET"),
    29.                   createRight("uc", "/getUserInfo", "GET"),
    30.                   createRight("uc", "/getDept", "GET"),
    31.                   createRight("uc", "/regrister", "POST"),
    32.                   createRight("oms", "/getDept", "GET"),
    33.                   createRight("oms", "/createOrder", "POST"),
    34.                   createRight("oms", "/getOrder", "GET")
    35.           );
    36.           PowerMockito.when(CacheUtils.getNewRole(ArgumentMatchers.anyLong())).thenReturn(BizConverUtils.getCacheRole(role, rightList));
    37.           Method method = PowerMockito.method(PowerFilter.class, "isUserRight",
    38.                   String.class,
    39.                   String.class,
    40.                   String.class,
    41.                   CacheGatewayUseDTO.class);
    42.           service = "uc";
    43.           path = "/getUserInfo4";
    44.           requestMethod = "GET";
    45.           // 无权限
    46.           gatewayUse = CacheGatewayUseDTO.builder()
    47.                   .userCode("")
    48.                   .nickname("")
    49.                   .roleIds(Arrays.asList(role.getId()))
    50.                   .userRights(Arrays.asList(
    51.                           UserRightsDO.builder().rightUrl("/getRoleList").requestMethod("GET").services(service).build()
    52.                           , UserRightsDO.builder().rightUrl("/getTest").requestMethod("GET").services(service).build()
    53.                           , UserRightsDO.builder().rightUrl("/getRights").requestMethod("GET").services(service).build()
    54.                   ))
    55.                   .build();
    56.           isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse);
    57.           log.info("isMatch {}", isMatch);
    58.           Assert.assertTrue(!isMatch);
    59.           // 直接权限
    60.           path = "/getRoleList";
    61.           isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse);
    62.           log.info("isMatch {}", isMatch);
    63.           Assert.assertTrue(isMatch);
    64.           // 角色权限,服务不一样,没有权限
    65.           path = "/getOrder";
    66.           isMatch = (boolean) method.invoke(powerFilter, service, path, requestMethod, gatewayUse);
    67.           log.info("isMatch {}", isMatch);
    68.           Assert.assertTrue(!isMatch);
    69.       }
    70.   }
    复制代码
    拔高篇——Spring Boot TestSptring Boot Test
      集成了 junit mockito 之大成,还有其他额外的能力。
      spring-boot-starter-test 包含了下面组件:

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









    本帖子中包含更多资源

    您需要 登录 才可以下载或查看,没有帐号?(注-册)加入51Testing

    x
    分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
    收藏收藏
    回复

    使用道具 举报

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条

    小黑屋|手机版|Archiver|51Testing软件测试网 ( 沪ICP备05003035号 关于我们

    GMT+8, 2024-5-2 21:20 , Processed in 0.069503 second(s), 23 queries .

    Powered by Discuz! X3.2

    © 2001-2024 Comsenz Inc.

    快速回复 返回顶部 返回列表