Java服务端单元测试指南(3)

发表于:2022-4-26 09:44

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:墨源    来源:网络

  4 单元测试内容
  在单元测试时,测试人员根据设计文档和源码,了解模块的接口和逻辑结构。主要采用白盒测试用例,辅之黑盒测试用例,使之对任何(合理和不合理)的输入都要能鉴别和响应。这就要求对程序所有的局部和全局的数据结构、外部接口和程序代码的关键部分进行检查。
  在单元测试中主要在5个方面对被测模块进行检查。
  4.1 模块接口测试
  在单元测试开始时,应该对所有被测模块的接口进行测试。如果数据不能正常地输入和输出,那么其他的测试毫无意义。Myers在关于软件测试的书中为接口测试提出了一个检查表:
  ·模块输入参数的数目是否与模块形式参数数目相同
  · 模块各输入的参数属性与对应的形参属性是否一致
  · 模块各输入的参数类型与对应的形参类型是否一致
  · 传到被调用模块的实参的数目是否与被调用模块形参的数目相同
  · 传到被调用模块的实参的属性是否与被调用模块形参的属性相同
  · 传到被调用模块的实参的类型是否与被调用模块形参的类型相同
  · 引用内部函数时,实参的次序和数目是否正确
  · 是否引用了与当前入口无关的参数
  · 用于输入的变量有没有改变
  · 在经过不同模块时,全局变量的定义是否一致
  · 限制条件是否以形参的形式传递
  · 使用外部资源时,是否检查可用性并及时释放资源,如内存、文件、硬盘、端口等
  当模块通过外部设备进行输入/输出操作时,必须扩展接口测试,附加如下的测试项目:
  · 文件的属性是否正确
  · Open与Close语句是否正确
  · 规定的格式是否与I/O语句相符
  · 缓冲区的大小与记录的大小是否相配合
  · 在使用文件前,文件是否打开
  · 文件结束的条件是否会被执行
  · I/O错误是否检查并做了处理
  · 在输出信息中是否有文字错误
  4.2 局部数据结构测试
  模块的局部数据结构是最常见的错误来源,应设计测试用例以检查以下各种错误:
  · 不正确或不一致的数据类型说明
  · 使用尚未赋值或尚未初始化的变量
  · 错误的初始值或错误的默认值
  · 变量名拼写错或书写错——使用了外部变量或函数
  · 不一致的数据类型
  · 全局数据对模块的影响
  · 数组越界
  · 非法指针
  4.3 路径测试
  检查由于计算、判定和控制流错误而导致的程序错误。由于在测试时不可能做到穷举测试,所以在单元测试时要根据“白盒”测试和“黑盒”测试用例的设计方法设计测试用例,对模块中重要的执行路径进行测试。重要的执行路径是通常指那些处在具体实现的算法、控制、数据处理等重要位置的路径,也可指较复杂而容易出错的路径。尽可能地对执行路径进行测试非常重要,需要设计因错误的计算、比较或控制流而导致错误的测试用例。此外,对基本执行路径和循环进行测试也可发现大量的路径错误。
  在路径测试中,要检查的错误有:死代码、错误的计算优先级、算法错误、混用不同类的操作、初始化不正确、精度错误——比较运算错误、赋值错误、表达式的不正确符号——>、>=;=、==、!=和循环变量的使用错误——错误赋值以及其他错误等。
  比较操作和控制流向紧密相关,测试用例设计需要注意发现比较操作的错误:
  · 不同数据类型的比较(注意包装类与基础类型的比较)
  · 不正确的逻辑运算符或优先次序
  · 因浮点运算精度问题而造成的两值比较不等
  · 关系表达式中不正确的变量和比较符
  · “差1错”,即不正常的或不存在的循环中的条件
  · 当遇到发散的循环时无法跳出循环
  · 当遇到发散的迭代时不能终止循环
  · 错误的修改循环变量
  4.4 错误处理测试
  错误处理路径是指可能出现错误的路径以及进行错误处理的路径。当出现错误时会执行错误处理代码,或通知用户处理,或停止执行并使程序进入一种安全等待状态。测试人员应意识到,每一行程序代码都可能执行到,不能自认为错误发生的概率很小而不进行测试。一般软件错误处理测试应考虑下面几种可能的错误:
  · 出错的描述是否难以理解,是否能够对错误定位
  · 显示的错误与实际的错误是否相符
  · 对错误条件的处理正确与否
  · 在对错误进行处理之前,错误条件是否已经引起系统的干预等
  在进行错误处理测试时,要检查如下内容:
  · 在资源使用前后或其他模块使用前后,程序是否进行错误出现检查
  · 出现错误后,是否可以进行错误处理,如引发错误、通知用户、进行记录
  · 在系统干预前,错误处理是否有效,报告和记录的错误是否真实详细
  4.5 边界测试
  边界测试是单元测试中最后的任务。代码常常在边界上出错,比如:在代码段中有一个n次循环,当到达第n次循环时就可能会出错;或者在一个有n个元素的数组中,访问第n个元素时是很容易出错的。因此,要特别注意数据流、控制流中刚好等于、大于或小于确定的比较值时可能会出现的错误。对这些地方需要仔细地认真加以测试。
  此外,如果对模块性能有要求的话,还要专门对关键路径进行性能测试。以确定最坏情况下和平均意义下影响运行时间的因素。下面是边界测试的具体要检查的内容:
  · 普通合法数据是否正确处理
  · 普通非法数据是否正确处理
  · 边界内最接近边界的(合法)数据是否正确处理
  · 边界外最接近边界的(非法)数据是否正确处理等
  · 在n次循环的第0次、第1次、第n次是否有错误
  · 运算或判断中取最大最小值时是否有错误
  · 数据流、控制流中刚好等于、大于、小于确定的比较值时是否出现错误
  5 单元测试规范
  5.1 命名规范
  · 目录结构:Maven目录结构下的单元测试目录“test”
  · 包名:被测试类包名
  · 类名:被测试类名 + Test
  · 方法名:test + 被测试方法名 + 4 + 测试内容(场景)
  5.2 测试内容
  第4部分概括的列举了需要测试的5大点内容,此处为服务端代码层至少要包含或覆盖的测试内容。
  Service
  · 局部数据结构测试
  · 路径测试
  · 错误处理测试
  · 边界测试
  HTTP接口
  · 模拟接口测试
  · 局部数据结构测试
  · 路径测试
  · 错误处理测试
  · 边界测试
  HSF接口
  · 模拟接口测试
  · 局部数据结构测试
  · 路径测试
  · 错误处理测试
  · 边界测试
  工具类
  · 模拟接口测试
  · 局部数据结构测试
  · 路径测试
  · 错误处理测试
  · 边界测试
  5.3 覆盖率
  为了使单元测试能充分细致地展开,应在实施单元测试中遵守下述要求:
  1、语句覆盖达到100%
  语句覆盖指被测单元中每条可执行语句都被测试用例所覆盖。语句覆盖是强度最低的覆盖要求,要注重语句覆盖的意义。比如,用一段从没执行过的程序控制航天飞机升上天空,然后使它精确入轨,这种行为的后果不敢想象。实际测试中,不一定能做到每条语句都被执行到。第一,存在“死码”,即由于代码设计错误在任何情况下都不可能执行到的代码。第二,不是“死码”,但是由于要求的输入及条件非常难达到或单元测试的实现所限,使得代码没有得到执行。因此,在可执行语句未得到执行时,要深入程序作做详细的分析。如果是属于以上两种情况,则可以认为完成了覆盖。但是对于后者,也要尽量测试到。如果以上两者都不是,则是因为测试用例设计不充分,需要再设计测试用例。
  2、分支覆盖达到100%
  分支覆盖指分支语句取真值和取假值各一次。分支语句是程序控制流的重要处理语句,在不同流向上设计可以验证这些控制流向正确性的测试用命。分支覆盖使这些分支产生的输出都得到验证,提高测试的充分性。
  3、覆盖错误处理路径
  即异常处理路径
  4、单元的软件特性覆盖
  软件的特性包括功能、性能、属性、设计约束、状态数目、分支的行数等。
  5、对试用额定数据值、奇异数据值和边界值的计算进行检验。用假想的数据类型和数据值运行测试,排斥不规则的输入。
  单元测试通常是由编写程序的人自己完成的,但是项目负责人应当关心测试的结果。所有的测试用例和测试结果都是模块开发的重要资料,需妥善保存。
  5.4 变异测试
  测试覆盖方法的确可以帮我们找到一些显而易见的代码冗余或者测试遗漏的问题。不过,实践证明,这些传统的方法只能非常有限的发现测试中的问题。很多代码和测试的问题在覆盖达到100%的情况下也无法发现。然而,“代码变异测试”这种方法可以很好的弥补传统方法的缺点,产生更加有效的单元测试。
  代码变异测试是通过对代码产生“变异”来帮助我们改进单元测试的。“变异”指的是修改一处代码来改变代码行为(当然保证语法的合理性)。简单来说,代码变异测试先试着对代码产生这样的变异,然后运行单元测试,并检查是否有测试是因为这个代码变异而失败。如果失败,那么说明这个变异被“消灭”了,这是我们期望看到的结果。否则说明这个变异“存活”了下来,这种情况下我们就需要去研究一下“为什么”了。
  总而言之,测试覆盖这种方法是一种不错的保障单元测试质量的手段。代码变异测试则比传统的测试覆盖方法可以更加有效的发现代码和测试中潜在的问题,它可以使单元测试更加强壮。
  6 CISE集成
  省略
  7 单元测试示例
  7.1 Service
  Service层单元测试示例。
  1.普通Mock测试:
  /**
  * 测试根据用户的Nick查询用户的图书列表方法
  * 其中“userService.getUserBooksByUserNick”方法最终需要通过UserId查询DB,
  * 所以在调用此方法之前需要先对UserService类的getUserIdByNick方法进行Mock。
  * 其中“bookDAO.getUserBooksByUserId”方法最终需要通过UserId查询DB,
  * 所以在调用此方法之前需要先对BookDAO类的getUserBooksByUserId方法进行Mock。
  */
  @Test
  public void testGetUserBooksByUserNick4Success() throws Exception {
  final List<BookDO> bookList = new ArrayList<BookDO>();
  bookList.add(new BookDO());
  new Expectations() {
  {
    userService.getUserIdByNick(anyString); // Mock的接口
    result = 1234567; // 接口返回值
    times = 1; // 接口被调用的次数
    bookDAO.getUserBooksByUserId(anyLong);
    result = bookList;
    times = 1;
  }
  };
  List<BookDO> resultBookList = bookService.getUserBooksByUserNick("moyuan.jcc");
  Assert.assertNotNull(resultBookList);
  }

  2.错误(异常)处理:
  /**
  * 测试根据用户的Nick查询用户的图书列表方法,注意在@Test添加expectedExceptions参数
  * 验证其中“userService.getUserBooksByUserNick”接口出现异常时,对异常的处理是否符合预期.
  * 其中“bookDAO.getUserBooksByUserId”方法不会被调用到。
  */
  @Test(expectedExceptions = {RuntimeException.class})
  public void testGetUserBooksByUserNick4Exception() throws Exception {
  final List<BookDO> bookList = new ArrayList<BookDO>();
  bookList.add(new BookDO());
  new Expectations() {
  {
    userService.getUserIdByNick(anyString); // Mock的接口
    result = new RuntimeException("exception unit test"); // 接口抛出异常
    times = 1; // 接口被调用的次数
    bookDAO.getUserBooksByUserId(anyLong);
    result = bookList;
    times = 0; // 上面接口出现异常后,此接口不会被调用
  }
  };
  List<BookDO> resultBookList = bookService.getUserBooksByUserNick("moyuan.jcc");
  Assert.assertNotNull(resultBookList);
  }

  3. Mock具体方法实现:
  /**
  * 测试发送离线消息方法
  * 消息队列:当离线消息超过100条时,删除最旧1条,添加最新一条。
  * 但消息存在DB或Tair中,所以需要Mock消息的存储。
  */ 
  @Test
  public void testAddOffLineMsg() throws Exception {
  final Map<Long, MsgDO> msgCache = new ArrayList<Long, MsgDO>();
  new Expectations() {
  {
      new MockUp<BookDAO>() {
          @Mock
          public void addMsgByUserId(long userId, MsgDO msgDO) {
             msgCache.put(userId, msgDO);
          }
      };
      new MockUp<BookDAO>() {
          @Mock
          public List<MsgDO> getUserBooksByUserId(long userId) {
             return msgCache.get(userId);
          }
      };
  }
  };
  final int testAddMsgCount = 102;
  for(int i = 0; i < testAddMsgCount; i++) {
  msgService.addMsgByUserId(123L, new MsgDO(new Date(), "this is msg" + i));
  }
  List<MsgDO> msgList = msgService.getMsgByUserId(123L);  
  Assert.assertTrue(msgList.size() == 100);
  new Verifications() {
  {
      // 验证 addMsgByUserId 接口是否被调用了100次
      MsgDAO.addMsgByUserId(anyLong, withInstanceOf(MsgDO.class));
      times = testAddMsgCount;
      // 验证是否对消息内容进行相就次数的转义
      SecurityUtil.escapeHtml(anyString);
      times = testAddMsgCount;
  }
  };
  }

  7.2 HTTP
  HTTP接口单元测试示例。
  1. Spring MVC Controller
  public final class BookControllerTest {
  @Tested(availableDuringSetup = true)
  private BookController bookController;
  @Injectable
  private BookService bookService;
  private MockMvc mockMvc;
  @BeforeMethod
  public void setUp() throws Exception {
  this.mockMvc = MockMvcBuilders.standaloneSetup(bookController).build();
  }
  /**
  *<strong>  </strong>********************************
  * getBookList unit test
  *<strong>  </strong>********************************
  */
  @Test
  public void testgetBookList4Success() throws Exception {
  new StrictExpectations() {
      {
          new MockUp<CookieUtil>(){
              @Mock
              public boolean isLogined(){
                  return true;
              }
          };
          userService.getUserBooksByUserNick(anyString);
          result = null;
          times = 1;
      }
  };
  ResultActions httpResult = this.mockMvc.perform(get("/education/getBookList.do?userNick=hello"))
      .andDo(print()).andExpect(status().isOk());
  MockHttpServletResponse response = httpResult.andReturn().getResponse();
  String responseStr = response.getContentAsString();
  // 如果存在多版本客户端的情况下,注意返回值向后兼容,此处需要多种格式验证.
  Assert.assertEquals(responseStr, "{\"code\":1,\"msg\":\"success\",\"data\":\"\"}");
  }
  }

  2. 参数化测试
  @DataProvider(name = "getBookListParameterProvider") 
  public Object[][] getBookListParameterProvider() {
  return new String[][]{
      {"hello", "{\"code\":1,\"msg\":\"success\",\"data\":\"\"}"},
      {"123", "{\"code\":301,\"msg\":\"parameter error\",\"data\":\"\"}"}
  };
  }
  @Test(dataProvider = "getBookListParameterProvider")
  public void testgetBookList4Success(String nick ,String resultCheck) throws Exception {
  new StrictExpectations() {
      {
          new MockUp<CookieUtil>() {
              @Mock
              public boolean isLogined() {
                  return true;
              }
          };
          userService.getUserBooksByUserNick(anyString);
          result = null;
          times = 1;
      }
  };
  ResultActions httpResult = this.mockMvc.perform(get("/education/getBookList.do?userNick=" + nick))
      .andDo(print()).andExpect(status().isOk());
  MockHttpServletResponse response = httpResult.andReturn().getResponse();
  String responseStr = response.getContentAsString();
  // 如果存在多版本客户端的情况下,注意返回值向后兼容,此处需要多种格式验证.
  Assert.assertEquals(responseStr, resultCheck);
  }

  7.3 工具类
  静态工具类测试示例。
  · 静态方法:
  java @Test public void testMethod() { new StrictExpectations(CookieUtil) { { CookieUtil.isLogined(); result = 
  或
<div>  java @Test public void testMethod() { new MockUp<CookieUtil>(){ @Mock public boolean isLogined(){ return true; </div><div></div>

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号