如何提高代码的UT测试覆盖率——Jalor5/6 Service的单元测试过程

举报
开发者学堂小助 发表于 2017/09/07 11:47:33 2017/09/07
【摘要】 Jalor 5/6 Service的单元测试骨架代码可以通过工具MyTester生成,​可以配置一个itest项目,用于代码的生成。开发完一个类后,只要修改对应的项目路径,执行main方法、再刷新项目即可(已经生成过测试代码的类将不作处理,只针对还未生成过测试代码的类),如下即为MyTester启动类的配置:

Jalor 5/6 Service的单元测试骨架代码可以通过工具MyTester生成可以配置一个itest项目,用于代码的生成。开发完一个类后,只要修改对应的项目路径,执行main方法、再刷新项目即可(已经生成过测试代码的类将不作处理,只针对还未生成过测试代码的类),如下即为MyTester启动类的配置:

下面是这次示例的待测试Service类:

-
Java 代码

01@Named
02@JalorResource(code = "COMMON", desc = "Common Service")
03public class SealService extends BaseService {
04    private static final ILogger logger = JalorLoggerFactory.getLogger(SealService.class);
05 
06    @Inject
07    IRegistyFilterQueryService registryQueryService;
08 
09   private static final String PATH = "PRMA.PartnerManage.SealToPublic.Path";
10 
11    /**
12     * 获取服务路径
13     *
14     * @return
15     */
16    private String getPath(String path) {
17        String pathValue = null;
18        try {
19            RegistryVO registryVO = registryQueryService.f in dRegistryByPath(path, false);
20            pathValue = registryVO.getValue();
21            if (null != registryVO && StringUtil.isNullOrEmpty(pathValue)) {
22                logger.error(PATH + " is NULL.");
23            }
24        } catch (Exception e) {
25            logger.error(e);
26        }
27        return pathValue;
28    }

下面是MyTester生成的单元测试骨架代码:

-
Java 代码

01/**
02* Test class for SealToPublicService.
03* @author z00315905
04*/
05public class SealServiceTest extends RequestTestSupport {
06    private static final Logger logger_ = Logger.getLogger(SealServiceTest.class);
07    @Spy
08    @InjectMocks
09    private SealService targetInstance;
10 
11    @Mock
12    private IRegistyFilterQueryService registryQueryService;
13 
14     
15    @Before
16    public void setUp() throws Exception {
17        MockitoAnnotations.initMocks(this);
18        super.initRequestContext();
19    }
20 
21    /**
22     * TestCase 1 for getPath.
23     */
24    @Test
25    public void getPath17_1() throws Exception {        
26         try {
27            String param10 = "";
28            invokeMethod(targetInstance, "getPath", param10);       
29        } catch (Exception e) {
30            logger_.error(e.getMessage(), e);
31        }  
32 
33    }
34     
35    /**
36     * TestCase 2 for getPath.
37     */
38    @Test
39    public void getPath17_2() throws Exception {        
40        try {
41            String param10 = null;
42            invokeMethod(targetInstance, "getPath", param10);        
43        } catch (Exception e) {
44            logger_.error(e.getMessage(), e);
45        }
46    }
47 
48 
49}

因为生成的代码只是一个骨架,所以全部放在try-catch中,故100%能通过。为了保证单元测试的有效性,测试代码需要不断完善,以下为具体操作步骤:
1、针对每个方法的每个用例,去掉骨架代码的try-catch;
2、执行测试用例,看看能否通过;
3、如果不通过,完善被测试代码,让其通过;
4、当所有生成的测试用例都 通过后,运行mvn test,查看测试用例执行的统计报告;
5、进一步完善测试用例,提升测试代码的覆盖率,如果达到100%,结束,否则,转步骤4。
接下来,我们按照这个步骤,逐步完善测试代码:

a)去掉第一个测试方法的try-catch,运行单元测试代码; 

-
Java 代码

1    /**
2     * TestCase 1 for getPath.
3     */
4    @Test
5    public void getPath17_1() throws Exception {        
6        String param00 = "";
7        invokeMethod(targetInstance, "getPath", param00);
8    }

运行后,发现没有报错;

b)去掉第二个测试方法的try-catch,再运行 

-
Java 代码

1    /**
2     * TestCase 2 for getPath.
3     */
4    @Test
5    public void getPath17_2() throws Exception {        
6        String param10 = null;
7        invokeMethod(targetInstance, "getPath", param10);
8    }

运行结果还是通过。

到此,生成的测试用例已经跑完了,没有发现问题,是不是就证明这个方法真的没有什么问题呢?显然不是,MyTester生成的只是非常简单的用例,可以称为冒烟用例。而实际的场景需要更多的用例。这就需要分析具体的方法逻辑。

-
Java 代码

01    private String getPath(String path) {
02        String pathValue = null;
03        try {
04            RegistryVO registryVO = registryQueryService.fi ndRegistryByPath(path, false);
05            pathValue = registryVO.getValue();
06            if (null != registryVO && StringUtil.isNullOrEmpty(pathValue)) {
07                logger.error(PATH + " is NULL.");
08            }
09        } catch (Exception e) {
10            logger.error(e);
11        }
12        return pathValue;
13    }

从代码来看,该方法就是通过数据字典的全路径,去数据库中查询对应的字符串值,如果查到了,返回对应的值,如果没查到或者出现异常(如数据库操作异常),返回null。按一般的测试逻辑,我们要先在数据库中插 入一条字典记录,然后再去查询,最后再验证取到的值和预期的值是否相等。这对于Service层的测试是不合理的,因为Service层主要是业务逻辑的处理,而不是这些基础设施(数据库操作、WebService操作、RPC操作等)的处理。也就是说,Service层的单元测试,要屏蔽掉一切基础设施,只测试自己的业务逻辑。即假定一切基础设施都是没问题的。那么,基础设施层的正确性靠什么来保证呢?是不是就不用测试呢?这是另一个话题,即基础设施层的单元测试(可以关注后期相关的文章)。

怎么做?Mock it!具体操作步骤如下:

a)先Mock出一个基础设施类(对于Service所需要调用的Dao/Service,MyTester已经创建好了);

b)对这个Mock类打桩(即设置预期,传入什么样的参数返回什么样的值);

c)执行测试,检验预期。

针对这个实例,我们需要做的第一件事(mock一个registryQueryService)已经完成;接下来就是打桩,针对这个registryQueryService.fi ndRegistryByPath(path, false)调用,不管path为多少,返回的结果5种,

第一种:返回RegistryVO为null;

第二种:返回RegistryVO非空,registryVO.getValue()为null;

第三种:返回RegistryVO非空,registryVO.getValue()为空字符串"";

第四种:返回RegistryVO非空,registryVO.getValue()不为null且不为空字符串;

第五种:抛出异常。

针对这四种情况,我们的方法调用,也应该返回对应的值,即:第一、二、五种情况,都应该返回null,第三种返回空字符串"";第四种,返回非null。如下是对应的打桩代码及测试代码(根据打桩后进行的方法调用结果是否符合预期):

-
Java 代码

01import static org.mockito.Mockito.*;
02import static org.springframework.test.util.ReflectionTestUtils.invokeMethod;
03import static org.junit.Assert.*;
04import static org.hamcrest.core.IsEqual.*;
05   
06    /**
07     * TestCase 3 for getPath.
08     */
09    @Test
10    public void getPath17_3() throws Exception {        
11        String path = "App.Version";
12        when(registryQueryService.fi ndRegistryByPath(path, false)).thenReturn(null);        
13        String actual = invokeMethod(targetInstance, "getPath", path);
14        assertNull(actual);        
15    }
16     
17    /**
18     * TestCase 4 for getPath.
19     */
20    @Test
21    public void getPath17_4() throws Exception {        
22        RegistryVO registryVo = new RegistryVO();
23        registryVo.setValue(null);
24        String path = "App.Version";
25        when(registryQueryService.fi ndRegistryByPath(path, false)).thenReturn(registryVo);        
26        String actual = invokeMethod(targetInstance, "getPath", path);
27        assertNull(actual);                
28    }
29 
30    /**
31     * TestCase 5 for getPath.
32     */
33    @Test
34    public void getPath17_5() throws Exception {        
35        RegistryVO registryVo = new RegistryVO();
36        registryVo.setValue("");
37        String path = "App.Version";
38        when(registryQueryService.fi ndRegistryByPath(path, false)).thenReturn(registryVo);        
39        String actual = invokeMethod(targetInstance, "getPath", path);
40        assertThat(actual, equalTo(""));
41    }
42     
43    /**
44     * TestCase 6 for getPath.
45     */
46    @Test
47    public void getPath17_6() throws Exception {
48        RegistryVO registryVo = new RegistryVO();
49        String expected = "1.0.1";
50        registryVo.setValue(expected);
51        String path = "App.Version";
52        when(registryQueryService.fi ndRegistryByPath(path, false)).thenReturn(registryVo);        
53        String actual = invokeMethod(targetInstance, "getPath", path);
54        assertThat(actual, equalTo(expected));
55    }

 到此,这个私有方法就完成了单元测试,运行下mvn test,看看JaCoCo的测试报告:


可以看到,代码的测试覆盖率已经达到了100%,到此,也的确说明该方法完成了单元测试,至于分支覆盖率,我们可以分析下,为什么只有75%,看下面的代码:

-
Java 代码

01String pathValue = null;
02        try {
03            RegistryVO registryVO = registryQueryService.fi ndRegistryByPath(path, false);
04            pathValue = registryVO.getValue();            
05            if (null != registryVO && StringUtil.isNullOrEmpty(pathValue)) {
06                logger.error(PATH + " is NULL.");
07            }
08        } catch (Exception e) {
09            logger.error(e);
10        }
11        return pathValue;

 if语句中,包含两部分,null != registryVO 和 StringUtil.isNullOrEmpty(pathValue),每部分包含两种取值(true/false),两两组合,应该包含4种情况。而实际上呢?当registryVO为null时,直接抛出异常了,所以,null != registryVO为false的情况就被跳过了,所以,4种就少了这一种,故分支覆盖率只有75%(即四分之三),解决的办法很简单,就是再加上一个判断,避免因为异常导致的跳过,如下:

-
Java 代码

01String pathValue = null;
02        try {
03            RegistryVO registryVO = registryQueryService.fi ndRegistryByPath(path, false);
04            if(registryVO != null){
05                pathValue = registryVO.getValue();
06            }            
07            if (null != registryVO && StringUtil.isNullOrEmpty(pathValue)) {
08                logger.error(PATH + " is NULL.");
09            }
10        } catch (Exception e) {
11            logger.error(e);
12        }
13        return pathValue;

 再来执行mvn test,看看测试报告

这个方法的单元测试终于结束了,这个类为了简化,只列出来一个私有方法,对于公共方法及逻辑更复杂的方法,要覆盖到所有的代码和分支,要求是比较高的,因此,要求具体的待测试方法,逻辑一定要清晰,代码不要太长(50行内),否则,编写单元测试将非常困难,按照测试驱动开发的思想,测试代码先行,将有助于编写逻辑清晰、易于测试的、高质量的代码。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。