Java服务端面试

随行付微服务测试之单元测试

2018-11-20  本文已影响231人  adf6f8243f49
本分类文章,与「随行付研究院」微信号文章同步,第一时间接收公众号推送,请关注「随行付研究院」公众号。
image

背景

单元测试为代码质量保驾护航,是提高业务质量的最直接手段,实践证明,非常多的缺陷完全可以通过单元测试来发现,测试金字塔提出者Martin Fowler 强调如果一个高层测试失败了,不仅仅表明功能代码中存在bug,还意味着单元测试的欠缺。因此,无论何时修复失败的端到端测试,都应该同时添加相应的单元测试。 而越早发现发现Bug,造成的浪费就会越小,单元测试本身就能够提供了快速反馈的机制。另外,单元测试是一个优秀的开发工程师必备技能之一,优秀的单元测试是业务快速投产的加速器。

微服务架构下开展单元测试的意义

虽然对于100%的单元测试覆盖率我们持有保留态度,但在一个微服务架构基础设施还不完善、开发人员能力参差不齐、DDD(领域驱动设计)能力不足以应对复杂业务的情况下,单元测试是性价比最高的实践。单元测试可以充当一个设计工具,它有助于开发人员去思考代码结构的设计,让代码更加有利于测试,满足架构的可测性设计要求。

单元测试的意义包括如下内容:

image

单元测试的常见误解

微服务架构下如何开展单元测试

下面将从单元测试所处的阶段、单元测试用例设计规范、单元测试实现几个维度分别介绍如何在微服务模式下开展单元测试。
首先看下单元测试所处的阶段,下图为非TDD模式下单元测试所处的阶段

image

由图可见单元测试处在特性分支开发完成之后,具体的描述如下:

下面看下什么样的单元测试用例是优秀的用例,是即满足运行速度又满足高覆盖率的用例。随行付定制了单元测试规范,下面节选了强制要求的部分规范。优秀的单元测试用例要符合以下用例设计规范的要求。

随行付在推行单元测试落地过程中采用循序渐进的方式,逐步增加单元测试用例达到单元测试规范中规定的覆盖率要求。需要说明的是我们不是追求覆盖率这个数字指标,那样就舍本求末了,我们是通过覆盖率这个可以量化的指标实现提高代码质量的这个根本目的。

随行付单元测试覆盖率统计同样采用SonarQube平台结合Jenkins工具,Jacoco单元测试覆盖率工具完成,这个同上篇介绍的静态代码扫描流程是一脉相承的。同时要求开发人员本地的IDE工具中安装Jacoco覆盖率插件,当本地开发完单元测试用例并构建后,即可看到覆盖率信息,进而可以快速补充用例,达到覆盖率要求。
以Eclipse为例,当开发完单元测试代码后,按照如下操作即可查看覆盖率信息。

  1.选择需要统计的java测试代码或者包;
  2.右键,Coverage as->Junit
  3.覆盖率结果会自动在Coverage 视图中展示出来;
  4.在Java编辑器中用不同的颜色标识代码的覆盖情况。
    【说明】 绿色----全覆盖
          红色----未覆盖
          黄色----部分覆盖
image

下面介绍下在微服务下应该如何进行单元测试。为了有效的进行单元测试,需要遵循一定的方法,通常采用路径覆盖法设计单元测试用例。所谓路径覆盖法就是选取足够多的测试数据,使程序的每条可能路径都至少执行一次(如果程序图中有环,则要求每个环至少经过一次)。具体设计过程参见如下步骤:

 1.画出程序控制流程图
 2.计算圈复杂度
 3.找出所有程序基本路径
 4.根据路径设计测试数据

以下图代码为例说明路径覆盖法的设计单元测试的过程

image
  1. 首先根据代码画出其对应的流程图如下,图中数字代表行号。当条件语句中包含多个条件时应予以拆分,如第13行,拆分为13.1和13.2;对于没有分支和循环的语句可忽略,如第16行。
image
  1. 有了流程图后,我们可以根据它计算出圈复杂度,这个可以作为测试用例数的上限,圈复杂度计算公式如下:

    V(G)= E - N + 2,E是流图中边的数量,N是流图中结点的数量。 V(G)= P + 1 ,P是流图G中判定结点的数量。

  2. 两个公式用哪个都行,最后的结果应该是一样的。这里我们用第二个公式,V(G)= 3 + 1 = 4,也就是我们只需要设计4条用例即可覆盖所有路径

  3. 接下来就是找出所有基本路径,基本路径是从程序的开始结点到结束可以选择任何的路径遍历,但是每条路径至少应该包含一条已定义路径不曾用到的边,所有的基本路径如下

    A
    B C
    B D E F
    B D E G E F

  4. 得到了所有的基本路径,剩下的简单了,只需要按照路径设计出对应的入参数据即可

    案例1:a = 0, b = 1, 期望值 -1

    案例2:a = 1, b = 0, 期望值 -1

    案例3: a = 4, b = 2, 期望值 2

    案例4:a = 8, b = 12, 期望值 4

除此之外,单元测试用例设计还需要考虑以下场景:

 边界值
     业务边界
     溢出边界
    字符串、数组、集合等的边界
异常场景
    业务异常
    输入异常(如参数不合法)
正常场景
    单个模块的用例设计都可以按照路径覆盖法达到语句覆盖和分支覆盖,但是对于有依赖关系的模块

在微服务模式下,每个模块之间会存在依赖的情况,为了保持单元测试的独立性原则,在不依赖于外部条件的情况下制造各种输入数据,需要借助Mock技术,其本质是用一个模拟的对象代替真实的对象(例如一个类、模块、函数或者微服务)。模拟对象的行为特征和真实对象非常相似,采用相同的调用逻辑,返回内容按照之前预定义的内容返回,提供返回数据。Mock技术的原理可以用如下案例进行解释。

image

当要进行单元测试时,需要给A注入B和C,但是C又依赖D,D又依赖E。这就导致了,A的单元测试不满足独立性原则。 但使用了Mock来进行模拟对象后,就可以把这种依赖解耦,只关心A本身的测试,它所依赖的B和C,全部使用Mock出来的对象,并且给MockB和MockC指定一个明确的行为。

image

在单元测试工具的选择方面,随行付单元测试借助Junit工具和Mockito工具进行单元测试,微服务模式下不管是spring boot还是spring cloud,通常使用@SpringBootTest注解进行单元测试。一个单元测试的实现步骤主要包括4步:

  1. 设置测试数据
  2. Mock依赖的系统并给定预期值,如果没有依赖这步可以省略
  3. 在测试中调用方法
  4. 断言返回的结果是否符合预期

下面以一个非常简单的例子介绍在微服务模式下如何对spring boot中的controller层和service层进行单元测试。

image

调用逻辑简化版如图所示,Controller调用ServiceA,ServiceA依赖ServiceB。

被依赖ServiceB的代码如下

  package cn.vbill.quality.service;
  import org.springframework.stereotype.Service;

  [@Service](https://my.oschina.net/service)
  public class ServiceB {
       public boolean serve(int param) {
       return param % 2 == 0;
       }
  }

被测ServiceA的代码如下

package cn.vbill.quality.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

[@Service](https://my.oschina.net/service)
public class ServiceA {
   @Autowired
   private ServiceB srvB;

   public String doSomething(int param) {
      if (srvB.serve(param)) {
        return "even";
      }
    return "obb";
  }
}

ServiceA和ServiceB的逻辑非常简单,现在测试ServiceA,步骤如下:

首先:在gradle中增加测试需要的依赖包

  // 可根据实际情况添加版本号
  testCompile("org.springframework.boot:spring-boot-starter-test")

其次:在src/test/java下面创建测试类,采用@SpringBootTest注解和Mockito技术对ServiceB进行测试和Mock,更多Mockito的使用可以参考其他文章,这里不过多介绍。代码如下:

 package cn.vbill.quality.service;
 import static org.junit.Assert.assertEquals;
 import static org.mockito.Mockito.when;
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.InjectMocks;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.junit4.SpringRunner;

 // 以下两个注解在Spring测试中可以说是固定写法
 @RunWith(SpringRunner.class)
 @SpringBootTest
 public class ServiceATest {
 @InjectMocks  //创建被测试类实例
 @Autowired
 private ServiceA srvA; // 先自动装配ServiceA,然后用Mock的ServiceB替换原来的ServiceB

 @Mock
 private ServiceB srvB; // 自动生成ServiceB的Mock实例

 @Before
 public void setup() {
    // 必须在ServiceA完成自动装配后在调用此方法
    // 处理@Mock注解,注入Mock对象
    MockitoAnnotations.initMocks(this);
 }

 // 用Mock对象替换真实的ServiceB可以轻松创造出我们所需的场景
 @Test
 public void doSomething_Even_Success() {
    // 设置Mock预期值
    when(srvB.serve(Mockito.anyInt())).thenReturn(true);
    // 因为Mock的缘故,此处doSomething的实参可随意写
    String result = srvA.doSomething(0);
    // 验证预期值
    assertEquals("even", result);
  }

@Test
public void doSomething_Obb_Success() {
    // 覆盖另一条分支
    when(srvB.serve(Mockito.anyInt())).thenReturn(false);
    String result = srvA.doSomething(0);
    assertEquals("obb", result);
  }

}

最后,使用覆盖率工具查看单元测试覆盖率,如下图所示,实现了100%覆盖。

image

ServiceB没有任何依赖,因此对它测试就按照常规的Junit测试即可,这里不过多介绍。下面介绍Controller层的单元测试,整体上看 Controller 层的测试和 Service 层大致相同,只不过是我们不去直接调用 Controller 的方法,而是通过MockMvc模拟HTTP请求。从逻辑图上看Controller是直接调用ServiceA,因此需要使用Mockito模拟ServiceA。

被测Controller代码逻辑如下:

package cn.vbill.quality.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import cn.vbill.quality.service.ServiceA;

@RestController
@RequestMapping("/")
public class DemoController {
@Autowired
private ServiceA srvA; // ServiceA 代码见上一节

@GetMapping
public String doSomething(@RequestParam("p") Integer param) {
    return srvA.doSomething(param);
  }
}

测试类如下

package cn.vbill.quality.web;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc // 使用该注解自动配置 MockMvc
public class DemoControllerTest {
 @Autowired
 @InjectMocks
 private DemoController controller;

 @Mock
 private ServiceA srvA;

@Autowired
private MockMvc mvc; // 自动配置 MockMvc

@Before
public void setup() {
    MockitoAnnotations.initMocks(this);
}

@Test
public void doSomething_Success() throws Exception {
    when(srvA.doSomething(Mockito.anyInt())).thenReturn("mock");
    // 使用MockMvc模拟HTTP请求
    // 下面三个类经常使用,常通过静态导入简化代码
    // MockMvcRequestBuilders, MockMvcResultHandlers, MockMvcResultMatchers
    mvc.perform(get("/").param("p", "1")).andExpect(content().string("mock"));
}
}

最后,通过覆盖率工具查看单元测试覆盖率为100%,做到了全覆盖。

image

以上是如何在微服务模式下进行单元测试进行了详细的介绍,在微服务架构下高覆盖率的单元测试是保障代码质量的第一道也是最重要的关口,应该持之以恒。

总结

本篇分别从微服务模式下开展单元测试的意义、对单元测试的常见误解以及如何开展单元测试三个方面进行介绍,单元测试是一项成本低、收益高的实践,要利用好这把利剑,打好代码质量基础,为后续的质量保证过程添砖加瓦。

image
上一篇 下一篇

猜你喜欢

热点阅读