网站首页 文章专栏 今天写点soul单元测试相关的- soul全局异常处理单元测试
今天写点soul单元测试相关的- soul全局异常处理单元测试

一. 单元测试的目的

单元测试是一种软件测试方法,通过该方法可以测试源代码的各个单元以确定它们是否适合使用。单元是最小的可测试软件组件,它通常执行单个内聚功能。单元测试就是是指对这个最小可测试组件——即单元进行检查和验证

单元测试拥有保证代码质量、尽早发现软件Bug、简化调试过程、促进变化并简化集成、使流程更灵活等优势

二. 单元测试名词说明

代码覆盖率:代码的覆盖程度,一种度量方式。

代码覆盖程度的度量方式是有很多种的,这里介绍一下最常用的几种:

1). 语句覆盖(StatementCoverage) 

    这是最常用也是最常见的一种覆盖方式,就是度量被测代码中每个可执行语句是否被执行到了,这里说的是“可执行语句”,只统计能够执行的代码被执行了多少行。需要注意的是,单独一行的花括号{} 也常常被统计进去。语句覆盖常常被人指责为“最弱的覆盖”,它只管覆盖代码中的执行语句,却不考虑各种分支的组合等等

2).  判定覆盖(DecisionCoverage)

又称分支覆盖(BranchCoverage),所有边界覆盖(All-EdgesCoverage),基本路径覆盖(BasicPathCoverage),判定路径覆盖(Decision-Decision-Path)。它度量程序中每一个判定的分支是否都被测试到了。这句话是需要进一步理解的,应该非常容易和下面说到的条件覆盖混淆。因此我们直接介绍第三种覆盖方式,然后和判定覆盖一起来对比,就明白两者是怎么回事了。

3). 条件覆盖(ConditionCoverage)

它度量判定中的每个子表达式结果true和false是否被测试到了。为了说明判定覆盖和条件覆盖的区别,我们来举一个例子,假如我们的被测代码如下:

4). 路径覆盖(PathCoverage)

又称断言覆盖(PredicateCoverage)。它度量了是否函数的每一个分支都被执行了。 这句话也非常好理解,就是所有可能的分支都执行一遍,有多个分支嵌套时,需要对多个分支进行排列组合,可想而知,测试路径随着分支的数量指数级别增加


举例:

int  foo( int  a,  int  b){
   int  nReturn  =   0 ;
   if (a < 10 ){ //  分支一
      nReturn += 1 ;
   }
   if (b < 10 ){ //  分支二
      nReturn += 10 ;
   }
   return  nReturn;
}

对上面的代码,我们分别针对我们前三种覆盖方式来设计测试案例:

1>. 语句覆盖

TestCase a = 5, b = 5   nReturn = 11
 语句覆盖率100%

2>. 判定覆盖

TestCase1 a = 5,   b = 5     nReturn = 11
TestCase2 a = 15, b = 15   nReturn = 0
判定覆盖率100%

3>. 条件覆盖

TestCase1 a = 5,   b = 15   nReturn = 1
TestCase2 a = 15, b = 5     nReturn = 10
条件覆盖率100%

4>. 路径覆盖

TestCase1 a = 5,    b = 5     nReturn = 0
TestCase2 a = 15,  b = 5     nReturn = 1
TestCase3 a = 5,    b = 15   nReturn = 10

TestCase4 a = 15,  b = 15   nReturn = 11
路径覆盖率100%


路径覆盖将所有可能的返回值都测试到了。这也正是它被很多人认为是“最强的覆盖”的原因了。

还有一些其他的覆盖方式,如:循环覆盖(LoopCoverage),它度量是否对循环体执行了零次,一次和多余一次循环。剩下一些其他覆盖方式就不介绍了。

三. 单元测试 19 法则:

1. 不应该编写成功通过的单元测试-它们应该被写成不通过的。你可以在几分钟内让任何一组测试通过,但这只是在欺骗你自己。

2. 测试类应该只测试一个功能-你应该用一个功能去测试一个方法。否则,你会违反了单一职责原则。

3. 测试类具备可读性-确保测试类标有注释并且容易理解,就像其他的代码一样。

4. 良好的命名规范-再次测试时应该像其他代码一样-便于人们理解。

5. 把断言从行为中分离出来-你的断言应该用来检验结果,而不是执行逻辑操作的。

6. 使用具体的输入-不要使用任何的自动化测试数据来输入,像date()这些产生的数据会引入差异。

7. 把测试类分类,放在不同的地方-从逻辑的角度看,当没有错误指向特定的问题时这更容易去查找。

8. 好的测试都是一些独立的测试类-你应该让测试类与其他的测试、环境设置等没有任何依赖。这利于创建多个测试点。

9. 不要包含私有的方法-他们都是一些具体的实现,不应该包含在单元测试里。

10. 不要连接数据库或者数据源-这是不靠谱的。因为你不能确保数据服务总是一样的并且能够创建测试点。

11. 一个测试不要超过一个模拟(mock对象)-我们努力去消除错误和不一致性。

12. 单元测试不是集成测试-如果你想测试结果,不要使用单元测试。

13. 测试必须具有确定性-你需要一个确定的预测结果,所以,如果有时候测试通过了,但是不意味着完成测试了。

14. 保持你的测试是幂等的-你应该能够运行你的测试多次而不改变它的输出结果,并且测试也不应该改变任何的数据或者添加任何东西。无论是运行一次还是一百万次,它的效果都应该是一样的。

15. 测试类一次仅测试一个类,测试方法一次仅测试一个方法-组织方法能够在问题出现时检测出来,并帮你确定测试依赖。

16. 在你的测试里使用异常-你在测试里会遇到异常,所以,请不要忽略它,要使用它。

17. 不要使用你自己的测试类去测试第三方库的功能-大多数好的库都应该有它们自己的测试,如果没考虑用mocks去产生一致性的结果的话。

18. 限制规则-当在一些规则下写测试时,记住你的限制和它们(最小和最大)设置成最大的一致性。

19. 测试类不应该需要配置或者自定义安装-你的测试类应该能够给任何人使用并且使它运行。“在我的机器上运行”不应该出现在这。

四. 我是怎么提交soul的单元测试的

我要测试的是GlobalErrorHandler,该类是对全局异常做出统一格式日志打印,以及返回参数处理

GlobalErrorHandler 继承了 DefaultErrorWebExceptionHandler,重写了3个方法,要测试的也是这三个

@Override
protected Map<String, Object> getErrorAttributes(final ServerRequest request, final boolean includeStackTrace) {
    logError(request);
    return response(request);
}

@Override
protected RouterFunction<ServerResponse> getRoutingFunction(final ErrorAttributes errorAttributes) {
    return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}

@Override
protected int getHttpStatus(final Map<String, Object> errorAttributes) {
    return HttpStatus.INTERNAL_SERVER_ERROR.value();
}

首先新建测试类,构建待测试的对象:

@RunWith(MockitoJUnitRunner.class)
public class GlobalErrorHandlerTest {

    private GlobalErrorHandler globalErrorHandler;
    ...
}

使用before注解,准备测试环境

@Before
public void setUp() {
    ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
    SpringBeanUtils.getInstance().setCfgContext(context);
    when(context.getBean(SoulResult.class)).thenReturn(new DefaultSoulResult() { });
    
    ErrorAttributes errorAttributes = new DefaultErrorAttributes();
    ResourceProperties resourceProperties = mock(ResourceProperties.class);
    ErrorProperties serverProperties = mock(ErrorProperties.class);
    ApplicationContext applicationContext = new AnnotationConfigReactiveWebApplicationContext();
    ViewResolver viewResolver = mock(ViewResolver.class);
    
    globalErrorHandler = new GlobalErrorHandler(errorAttributes, resourceProperties, serverProperties, applicationContext);
    globalErrorHandler.setViewResolvers(Collections.singletonList(viewResolver));
}

此处有巨坑,DefaultErrorAttributes对象不能 mock()出来,mock出来的对象在实际测试过程中,一直报nullpointexcept,new出来就可以了。

还有测试的方法中使用了其他的工具类,工具类有依赖别的静态方法,需要提前mock好

@Test
public void getErrorAttributes() {
    ServerWebExchange webExchange =
            MockServerWebExchange.from(MockServerHttpRequest
                    .post("http://localhost:8080/favicon.ico"));
    MockServerRequest serverRequest = MockServerRequest.builder()
            .exchange(webExchange)
            .attribute("org.springframework.boot.web.reactive.error.DefaultErrorAttributes.ERROR", new NullPointerException("errorMessage"))
            .build();

    Map<String, Object> response = globalErrorHandler.getErrorAttributes(serverRequest, false);
    Assert.assertNotNull(response);
    Assert.assertEquals(response.get("code"), (long) HttpStatus.INTERNAL_SERVER_ERROR.value());
    Assert.assertEquals(response.get("message"), HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase());
    Assert.assertEquals(response.get("data"), "errorMessage");
}

    

对重写的getErrorAttributes方法进行测试,构建web请求,并在请求中模拟异常,并对结果进行断言

其他方法类似,写完本地运行下,没问题,再检查下checkstyle,就可以提交了

我提交后,发现github的CI没过,发现原来是apache 的license没写,加上后就行了,然而尴尬的是,覆盖率下降了。。。

明天再改改吧



版权声明:本文由星尘阁原创出品,转载请注明出处!

本文链接:http://www.52xingchen.cn/detail/72




赞助本站,网站的发展离不开你们的支持!
来说两句吧
大侠留个名吧,或者可以使用QQ登录。
: 您已登陆!可以继续留言。
最新评论