Jalor 5/6 Service的单元测试骨架代码可以通过工具MyTester生成,可以配置一个itest项目,用于代码的生成。开发完一个类后,只要修改对应的项目路径,执行main方法、再刷新项目即可(已经生成过测试代码的类将不作处理,只针对还未生成过测试代码的类),如下即为MyTester启动类的配置:
下面是这次示例的待测试Service类:
-
Java 代码02 | @JalorResource (code = "COMMON" , desc = "Common Service" ) |
03 | public class SealService extends BaseService { |
04 | private static final ILogger logger = JalorLoggerFactory.getLogger(SealService. class ); |
07 | IRegistyFilterQueryService registryQueryService; |
09 | private static final String PATH = "PRMA.PartnerManage.SealToPublic.Path" ; |
16 | private String getPath(String path) { |
17 | String pathValue = null ; |
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." ); |
24 | } catch (Exception e) { |
下面是MyTester生成的单元测试骨架代码:
-
Java 代码02 | * Test class for SealToPublicService. |
05 | public class SealServiceTest extends RequestTestSupport { |
06 | private static final Logger logger_ = Logger.getLogger(SealServiceTest. class ); |
09 | private SealService targetInstance; |
12 | private IRegistyFilterQueryService registryQueryService; |
16 | public void setUp() throws Exception { |
17 | MockitoAnnotations.initMocks( this ); |
18 | super .initRequestContext(); |
22 | * TestCase 1 for getPath. |
25 | public void getPath17_1() throws Exception { |
28 | invokeMethod(targetInstance, "getPath" , param10); |
29 | } catch (Exception e) { |
30 | logger_.error(e.getMessage(), e); |
36 | * TestCase 2 for getPath. |
39 | public void getPath17_2() throws Exception { |
41 | String param10 = null ; |
42 | invokeMethod(targetInstance, "getPath" , param10); |
43 | } catch (Exception e) { |
44 | logger_.error(e.getMessage(), e); |
因为生成的代码只是一个骨架,所以全部放在try-catch中,故100%能通过。为了保证单元测试的有效性,测试代码需要不断完善,以下为具体操作步骤:
1、针对每个方法的每个用例,去掉骨架代码的try-catch;
2、执行测试用例,看看能否通过;
3、如果不通过,完善被测试代码,让其通过;
4、当所有生成的测试用例都 通过后,运行mvn test,查看测试用例执行的统计报告;
5、进一步完善测试用例,提升测试代码的覆盖率,如果达到100%,结束,否则,转步骤4。
接下来,我们按照这个步骤,逐步完善测试代码:
a)去掉第一个测试方法的try-catch,运行单元测试代码;
-
Java 代码2 | * TestCase 1 for getPath. |
5 | public void getPath17_1() throws Exception { |
7 | invokeMethod(targetInstance, "getPath" , param00); |
运行后,发现没有报错;
b)去掉第二个测试方法的try-catch,再运行
-
Java 代码2 | * TestCase 2 for getPath. |
5 | public void getPath17_2() throws Exception { |
7 | invokeMethod(targetInstance, "getPath" , param10); |
运行结果还是通过。
到此,生成的测试用例已经跑完了,没有发现问题,是不是就证明这个方法真的没有什么问题呢?显然不是,MyTester生成的只是非常简单的用例,可以称为冒烟用例。而实际的场景需要更多的用例。这就需要分析具体的方法逻辑。
-
Java 代码01 | private String getPath(String path) { |
02 | String pathValue = null ; |
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." ); |
09 | } catch (Exception e) { |
从代码来看,该方法就是通过数据字典的全路径,去数据库中查询对应的字符串值,如果查到了,返回对应的值,如果没查到或者出现异常(如数据库操作异常),返回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 代码01 | import static org.mockito.Mockito.*; |
02 | import static org.springframework.test.util.ReflectionTestUtils.invokeMethod; |
03 | import static org.junit.Assert.*; |
04 | import static org.hamcrest.core.IsEqual.*; |
07 | * TestCase 3 for getPath. |
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); |
18 | * TestCase 4 for getPath. |
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); |
31 | * TestCase 5 for getPath. |
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( "" )); |
44 | * TestCase 6 for getPath. |
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)); |
到此,这个私有方法就完成了单元测试,运行下mvn test,看看JaCoCo的测试报告:
可以看到,代码的测试覆盖率已经达到了100%,到此,也的确说明该方法完成了单元测试,至于分支覆盖率,我们可以分析下,为什么只有75%,看下面的代码:
-
Java 代码01 | String pathValue = null ; |
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." ); |
08 | } catch (Exception e) { |
if语句中,包含两部分,null != registryVO 和 StringUtil.isNullOrEmpty(pathValue),每部分包含两种取值(true/false),两两组合,应该包含4种情况。而实际上呢?当registryVO为null时,直接抛出异常了,所以,null != registryVO为false的情况就被跳过了,所以,4种就少了这一种,故分支覆盖率只有75%(即四分之三),解决的办法很简单,就是再加上一个判断,避免因为异常导致的跳过,如下:
-
Java 代码01 | String pathValue = null ; |
03 | RegistryVO registryVO = registryQueryService.fi ndRegistryByPath(path, false ); |
04 | if (registryVO != null ){ |
05 | pathValue = registryVO.getValue(); |
07 | if ( null != registryVO && StringUtil.isNullOrEmpty(pathValue)) { |
08 | logger.error(PATH + " is NULL." ); |
10 | } catch (Exception e) { |
再来执行mvn test,看看测试报告
这个方法的单元测试终于结束了,这个类为了简化,只列出来一个私有方法,对于公共方法及逻辑更复杂的方法,要覆盖到所有的代码和分支,要求是比较高的,因此,要求具体的待测试方法,逻辑一定要清晰,代码不要太长(50行内),否则,编写单元测试将非常困难,按照测试驱动开发的思想,测试代码先行,将有助于编写逻辑清晰、易于测试的、高质量的代码。
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
评论(0)