文章摘要:追求代码质量一直都是优秀程序员对自己的目标,那么有什么好方法能够实现这个目标?
在每个系统上线正式发布之前,开发同事对其中功能点进行自测,测试同事根据前期设计的测试用例进行功能测试的都是保障系统可靠稳定运行的重要前提。但是,系统上线后故障还偶有发生,那么如何才能将系统代码质量提高一个档次做到上线后0故障的目标呢?我想这个问题一直是许多研发同学和测试同学共同追求的一个目标,但光靠代码review、简单的自测和功能测试用例覆盖还是不够,需要从代码覆盖率(包括语句覆盖率、分支覆盖率和路径覆盖率等)的角度来解决。因此,本文从解决问题的根本原因出发介绍以SpringBoot工程的自动化单元测试用例结合Cobetura插件来实现定时跑测试任务并生成测试报告。
追求代码质量是一个优秀程序员对自我的要求。我们写一段代码、一个方法和一个类,不仅仅说完成了编码,保证代码能正常得跑起来就行了,而且也必须使得代码是优雅和干净的。一般来说正常的情况大家都能考虑到,比较关键和重要的是,我们在写代码时除了能够执行正常业务逻辑以外,还要能考虑和覆盖到各种不同的异常情况。我想在编码时候,考虑正常和异常情况的时间分配比例应该是30%:70%。
从主观上来说,代码质量一般是跟程序员的专业技能熟练度,比如编程语言(C++/Java/go等)、技术框架(Spring/Dubbo/Spring Cloud/Spring Boot等)、设计模式(工厂/抽象/代理模式等),成正比的。但是,对于极为优秀的程序员来说即使能够尽可能地确保自己的千行代码没有缺陷,却不一定能够保证几万行都没有任何缺陷。所以,我们需要借鉴其他的方法来提高自己的代码质量,尽可能少地让潜在的问题暴露在生产环境上。
增加功能测试用例和接口单元测试都是能够提高代码质量的方式,各有优劣。本文从编程者的角度出发,更加注重的是代码覆盖测试,毕竟只有写代码的人才能更容易地把控代码中的业务逻辑,能够更好的编写单元测试用例以覆盖正常和异常的业务场景。
在做单元接口测试时,代码覆盖率常常是被拿来作为衡量测试好坏的指标,甚至,用代码覆盖率来考核测试任务完成情况。通常来说,我们会关注方法覆盖、语句覆盖、条件覆盖和分支覆盖这几种度量方式。
本文第一节主要都是讲了理论,相对比较枯燥。下面这一节将从实践的角度,来一步一步向大家展示如何在Spring Boot工程中对业务代码写单元测试用例。
Spring Boot 1.4.1.RELEASE、JDK1.8
在Spring Boot工程中引入单元测试比较简单,只需要简单地在pom文件中引入依赖如下:
在工程中引入spring-boot-starter-test后,就会有如下几个库:
(a)JUnit:Java语言的单元测试框架;
(b)SpringTest & Spring Boot Test:为Spring Boot程序提供集成测试的工具;
(c)AssertJ: 一种断言库;
(d)Hamcrest:也是一种断言库,不过更新频度较慢;
(e)Mockito :Java程序Mock测试的框架;
(f)JsonPath :Xpath在Json中的应用库;
(g)JSONassert:Json的断言库;
spring-boot-starter-test的pom依赖图如下:
对于Spring Boot工程中的Service/Dao层的类来说,创建其单元测试方法比较简单,直接手动创建即可。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = OpHuaweiAgentApplication.class)
@Slf4j
public class VmServiceTest {
private ObjectMapper mapper = new ObjectMapper();
private static final String JSON_CREATE_VM_REQUEST = "/vm/createVmReq.json";
@Autowired
private VmService vmService;
@Before
public void setUp() throws Exception {
//测试用例数据准备
//code here
}
@Test
public void create() throws Exception {
String createBody = getResource(JSON_CREATE_VM_REQUEST);
JSONObject body = JSONObject.parseObject(createBody);
HwTokenWrapper token = getDomainToken();
String projectId = token.getToken().getProject().getId();
CreateJobRespDto respDto = vmService.create(token.getId(), projectId, body);
assertTrue(null != respDto.getJobId());
log.debug("create vm job创建成功,jobId:{}", respDto);
}
@After
public void cleanUp() throws Exception(){
//测试数据清理
//code here
}
如上面的对Service层的单元测试用例代码可见,在带有@Before注解的方法setUp中完成对测试用例的数据准备,可以提前在测试环境数据库中插入测试用例所需依赖的测试局数据。在@Test注解的方法—create是单元测试真正执行的方法,示例中使用提前组织好的创建主机规格的Json数据作为参数调用被测试的Service层的VmService方法,执行创建主机的验证。同时使用断言机制,来判断返回结果是否跟预期的一致。其中,准备好的Json数据放在SpringBoot工程的src/test/resources下面。最后在,@After注解的方法cleanUp下执行提前插入数据的回滚和清理。
对Service/Dao层的类进行接口单元测试还是比较简便的。然而,一般的SpringBoot工程都需要对外部提供Api接口,因此有必要对Controller层进行单元测试以保证控制器执行的业务逻辑正确,这时候就得用到MockMvc了。使用MockMvc可以使得开发或者测试不必再借助postman这种Http调试工具进行手动测试,既提高测试的效率,也能够反复跑单元测试用例来进行回归验证。
Spring Test框架中的MockMvc实现了对Http请求的模拟,能够直接通过网络的形式,转换到Controller层的Api调用,这样在提高测试效率的同时可以不依赖外部环境。
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = OpHuaweiAgentApplication.class)
@WebAppConfiguration
@Slf4j
public class OrderManageControllerTest extends AbstractTest {
//此处API URL为示例代码
public static final String GENERATE_ORDERID_API_URL = "/rest/xxxxxxxxx";
//此处为鉴权的Json测试数据
private static final String JSON_AUTH_TOKEN_REQ = "/api/order/authTokenReq.json";
@Autowired
private WebApplicationContext context;
private MockMvc mvc;
@Before
public void setUp() {
//测试用例数据准备
//code here
this.mvc = MockMvcBuilders.webAppContextSetup(this.context).build();
}
@Test
public void generateOrderIdTest() throws Exception {
JSONObject jsonObject = JSONObject.parseObject(getResource(JSON_AUTH_TOKEN_REQ));
MockHttpServletRequestBuilder builder = post(GENERATE_ORDERID_API_URL). header("X-Auth-Token", jsonObject.getString("token"));
MvcResult result = mvc.perform(builder).andReturn();
assertEquals(HttpStatus.OK.value(), result.getResponse().getStatus());
}
@After
public void cleanUp() throws Exception(){
//测试数据清理
//code here
}
从上面对Controller层Api接口的单元测试示例代码可见,在带有@Before注解的setUp方法中,通过MockMvcBuilders工具类使用注入的WebApplicationContext上下文对象创建MockMvc对象。这里,MockMvc对象提供一组工具函数用来执行assert判断,都是针对web请求的判断。这组工具的使用方式是函数的链式调用,允许程序员将多个测试用例链接在一起,并进行多个判断。在带有@Test注解的generateOrderIdTest测试方法中,先加载提前准备好的鉴权请求JsonObject对象,然后MockMvc对象执行相应的post请求,其中参数为带有Header头的MockHttpServletRequestBuilder对象。最后,通过assertEquals断言机制来确认接口返回是否为Http响应的正确编码(200)。如同之前的一样,@After注解的方法cleanUp下执行提前插入数据的回滚和清理。
通过上面的内容,可以在Spring Boot工程中完成对Controller/Service/Dao层的添加单元测试用例,但仅限于此只能通过单元测试用例的结果(是否跑成功)来判断用例正确与否,而无法来判断测试的其他度量指标,比如本文前面提到的方法覆盖、语句覆盖、条件覆盖和分支覆盖等。因此,这节通过引入第三方组件—Cobertura来完成这一目标。
Cobertura 是一种开源的代码覆盖率检测工具,它通过检测基本的代码,并观察在测试包运行时执行了哪些代码和没有执行哪些代码,并最终以html或者xml的格式来呈现最终测试的度量指标结果(比如分支覆盖率和代码行覆盖率)。
在Spring Boot工程的pom文件中添加Cobertuar插件的配置如下:
在Spring Boot工程目录下执行以下maven命令—“mvn cobertura:cobertura”,执行完后会在target目录里找到site目录,用浏览器打开里面的index.html,这就是测试用例执行完后cobertura-maven-plugin得出的覆盖率报告。如图下所示:
仅在本地对Spring Boot工程执行Cobertura的maven命令,并不能很好的实现自动持续集成的目标。这一节主要将介绍如何在Jenkins工具中一步步集成Cobertura插件并完成Spring Boot工程的代码覆盖率测试报告输出。
1、首先需要在Jenkins工具上完成Cobertura插件的安装。
2、配置jenkins工具,修改maven的执行命令,这里主要是添加cobertura执行命令clean cobertura:cobertura package。
3、在Add post build action这个配置项中选择如下Publish Cobertura Coverage Report:
4、这一步中需要选择一个配置项,该配置项目是最终cobertura生成xml/html report的路径,在示例中的路径为**/target/site/cobertura/coverage.xml。
5、最后,重新build该项目,即可在项目中看到本工程代码覆盖率的测试用例报告了:
本文从代码质量与单元测试用例方面切入,先介绍了如何在Spring Boot工程中完成各层(Controller Api/Service/Dao层)的接口单元白盒测试,随后介绍了如何在Spring Boot工程中集成Cobertura插件,并利用Jenkins工具进行自动化持续集成以产生代码覆盖率的测试报告。限于笔者的才疏学浅,对本文内容可能还有理解不到位的地方,如有阐述不合理之处还望留言一起探讨。