公司最近很重视代码质量问题,自然的对单元测试要求也是越来越高,对新代码的Merge Request设置了Line Coverage >= 80%,Branch Coverage >= 80%的门槛。由于事出突然,身边的小伙伴们对此也是频频叫苦......毒害最深的要数Branch Coverage要求了,这也让我们深刻地体会到了UT的不简单。

  言归正传,公司统一采用Jacoco来统计各项的覆盖率,包括类(Classes)、行(Lines)、方法(Methods)、指令(Instructions),分支(Branches)、圈复杂度(Cyclomatic Complexity)等维度。

  Jacoco的pom.xml文件配置也很简单,网上一搜一大把,这边随意贴一个:

<plugin>
        <groupId>org.jacoco</groupId>
        <artifactId>jacoco-maven-plugin</artifactId>
        <version>0.8.2</version>
    <configuration>
        <destFile>target/coverage-reports/jacoco-unit.exec</destFile>
        <dataFile>target/coverage-reports/jacoco-unit.exec</dataFile>
    </configuration>
    <executions>
        <execution>
            <id>jacoco-initialize</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>jacoco-site</id>
            <phase>package</phase>           
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
    </executions>
</plugin>

  与Maven集成后,可以使用mvn install指令跑UT结果,会在report文件下生成一个site文件夹,如图:

  

单元测试分支覆盖率工具类java 单元测试覆盖度_工具类

单元测试分支覆盖率工具类java 单元测试覆盖度_工具类_02

  点开其中的jacoco文件夹,打开index.html文件即可查看覆盖率情况。

  当然这都不是本文的重点,有需要了解更加细节的内容可自行查阅。今儿主要是想介绍下Branch Coverage的麻烦之处,因为在提Merge Request的时候经常碰到分支覆盖率不达标的情况,于是本地跑了UT结果之后,查看了具体情况,如图:

  

单元测试分支覆盖率工具类java 单元测试覆盖度_工具类_03

  Jacoco有三种棱形图案来表示Branch Coverage:

  1、红色棱形:没覆盖

  2、黄色棱形:覆盖了部分

  3、绿色棱形:全覆盖

  如图中所示,黄色棱形经常伴随着1 of n branches missed的提示,就是告诉你还有几个逻辑没覆盖到。举个例子:

if (A && B && C) {
    // xxx
}

  上述代码如果想要分支覆盖率达到100%,就需要覆盖以下情况:

  1、A满足,B满足,C满足

  2、A不满足

  3、A满足,B不满足

  4、A满足,B满足,C不满足

  对于或条件的例子:

if (A || B || C) {
    //.....    
}

  1、A不满足,B不满足,C不满足

  2、A满足

  3、A不满足,B满足

  4、A不满足,B不满足,C满足

  举的例子都是if语句的,对于switch也是类似的情况,如果switch的条件中有多个情况,也都需要统统走一遍。总而言之,想要把分支覆盖率达到100%,就必须把所有的逻辑场景都考虑到,所以写UT并没有想象中的那么简单,业内有理论说业务开发时间与UT时间应该是1:1的关系,一点不假!

  从我自身实际写UT的感受中,简单说下UT带来的好处:

  1、如果是认真写UT,且覆盖率全面的话,确实能发现业务代码中的bug

  2、如果是为他人代码写UT,有助于熟悉之前不了解的代码逻辑

  3、锻炼思维逻辑,考虑会更加周到

 


  

  我在写UT的时候采用了Mockito与PowerMockito相结合的方式,对于一些static、final类,或者一些static方法,可以使用PowerMockito去mock,十分方便。

  比如我们当前正在写A类print方法的UT,其中用到了如下工具类的getImageUrl方法,我们需对其进行mock:

// 工具类
public final class ImageUtil {
    private ImageUtil(){}
    public static String getImageUrl(String image) {
        // ....    
    }
}

// A
public class A {
    // ....

  // print
  public String print() {
    // ....
    ImageUtil.getImageUrl(xxx);
    // ....
  }
}

// 测试
@RunWith(PowerMockRunner.class)
@PrepareForTest({ImageUtil.class})
public class ATest {
    @InjectMocks
    private A a; // 被测试类
    
    @Before
    public void setup(){
        PowerMockito.mockStatic(A.class); // 准备
    }

  @Test
  public void testPrint() {
    Mockito.when(ImageUtil.getImageUrl(Mockito.anyString())).thenReturn("Anything you want...");
  }
}

  当然使用PowerMockito还能相对简单的对私有方法进行测试,原理是基于反射。但即使不用PowerMockito也能自行通过reflect机制对私有方法进行测试。

 


 

   就像我标题写的“两三事”一样,本文只是很随意的记录一些实际写UT过程中碰到的一些问题或者有趣的事情,如果想要看到很强的理论知识,那恐怕会让你失望了。

  前几天又用到了一个新的mock手段,“Mockito.RETURNS_DEEP_STUBS”。

  我们在写UT的时候,经常会出现如下情况:

// 假设有一个企业对象
Company company = .....;

// 获取该企业内的某一个事业部下的某一个部门里某团队的某一个人的姓名
company.getBU().getDepartment().getTeam().getSomeOne().getName()

  对于一长串的调用的mock,“Mockito.RETURNS_DEEP_STUBS”能很方便的搞定。

// 深度打桩
Company company = Mockito.mock(Company.class, Mockito.RETURNS_DEEP_STUBS);

// mock
Mockito.when(company.getBU().getDepartment().getTeam().getSomeOne().getName()).thenReturn("小明");

  而且不需要担心会有NPE问题的抛出,十分方便!