当代程序开发中,随着程序开发的迭代。传统的测试已经没办法满足项目的测试要求。尤其是大型项目,每次迭代后往往都会出现原本功能正常的地方出现bug。从而导致程序维护成本不断升高,运维效率不断降低,风险不断增高。
随着项目的不断更新,测试覆盖率会越来越难以保证,而且随着时间的推移,这些问题会严重的阻碍系统的升级和优化。
既然程序的开发和迭代有那么大的困难,那么我们有没有较好的手段解决这个问题呢?当然有,就是我即将介绍的junit。作为自动化的单元测试工具,junit做到了一次用例设计,以后可以反复测试执行。并且可以借助插件生成相应的单元测试报告。
今天就通过一个简单的例子介绍junit的相关使用技巧。
讲解的例子为闰年的判断逻辑:
- 闰年应该可以被4整除。
- 年份如果为世纪,则当年份与400相除时不能除尽的时候为平年,否则为闰年。
上述逻辑实现的代码如下:
/**
* 判断是否为闰年。 如果不是世纪(百年),能被四整除的则是闰年。否则为平年。
* 世纪的,如果能被400整除的,为闰年,否则为平年。
*
* @param year 年份
* @return 如果是闰年返回true,否则返回false。
*/
public static boolean isLeapYear(int year) {
boolean flag = false;
if (year % 4 != 0) {
flag = false;
} else if (year % 100 != 0) {
flag = true;
} else if (year % 100 == 0 && year % 400==0) {
flag = true;
} else {
flag = false;
}
return flag;
}
通常我们通过main方法进行测试,代码如下。
public static void main(String[] args) {
System.out.println(isLeapYear(1900)); // false
System.out.println(isLeapYear(1901)); // false
System.out.println(isLeapYear(1904)); // true
System.out.println(isLeapYear(2000)); // true
}
测试运行结果:
false
false
true
true
这种测试的缺陷在于,需要人为去观察运行结果,和确定运行结果的准确性。
项目中使用junit,需要引用相关的项目,引用依赖如下:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
如果使用junit,那么每个测试用例的代码应该是这样的:
@Test
public void testNotLeapNormal() {
Assert.assertFalse(DateUtil.isLeapYear(1901));
}
@Test
public void testNotLeapCentury() {
Assert.assertFalse(DateUtil.isLeapYear(1900));
}
@Test
public void testLeapNormal() {
Assert.assertTrue(DateUtil.isLeapYear(1904));
}
@Test
public void testLeapCentury() {
Assert.assertTrue(DateUtil.isLeapYear(2000));
}
运行结果:
如果程序逻辑调整,运行结果出现偏差,那么也会知道是哪段执行逻辑出现了问题:
上述结果是我把代码
year % 100 == 0 && year % 4000 改成 year % 100 == 0 || year % 4000 之后的运行结果。一旦代码逻辑出现问题,那么就会知道修改的代码导致了运行结果和预期不符。
使用junit后如果是代码逻辑调整或者新增我们只需要调整响应的用例或者新增相应的用例即可。如果是逻辑优化,我们也可以保证优化的质量。
上述代码的最大问题就是代码的判断嵌条。使用局部变量,导致方法过长,不通代码段之间相互影响,可读性差。
我们可以对代码进行调整,调整后的代码如下:
if (year % 4 != 0) {
return false;
}
if (year % 100 != 0) {
return true;
}
if (year % 400==0) {
return true;
}
return false;
}
这样,我们逻辑做了调整之后,测试代码不变,原则上也不调整逻辑,所以修改完成后只需要再次运行测试用例通过即可。
通常做优化,如果是没有测试用例,我们建议的是先编写测试用例,然后再开始优化。
至此,基本的逻辑测试介绍就是这样的了,如果是逻辑拓展,并且拓展逻辑使用了我们已经测试过的方法。我们就可以通过mock进行相关的测试了。
例如:我们想通过方法计算相关年份的的总天数(闰年366天,平年365天),相关代码如下:
public static int getDayNum(int year) {
if (isLeapYear(year)) {
return 366;
}
return 365;
}
如果按常规测试,那么我们需要根据所有情况的年份来测试日期,这样的话,用例上面和判断闰年的用例重叠。只是判断的结果稍有不同而已。那么会出现用例上的重复。
所以对于方法调用来说,我只需要关心我调用的方法结果正确的情况下我们所开发的方法正确即可,如果说调用的方法本身有错误,应该属于该方法开发者去关心的。不应该是方法调用者需要关心的问题。所以,我们只需要保证这个方法返回true时我们返回366,其他情况返回365即可。这时,我们需要用到mock。
mock有jmockit,power mock, easy mock等。power mock对于对象方法的mock更有优势,easy mock对于静态方法的mock更方便。jmockit基本可以满足上述所说的所有情况。引用jmockit需要添加如下依赖:
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.8</version>
<scope>test</scope>
</dependency>
使用jmockit时,在类上应该添加注解:
@RunWith(JMockit.class)
针对上述代码,我们测试的用例编写如下:
@Test
public void testDayLeap() {
new Expectations(DateUtil.class) {
{
DateUtil.isLeapYear(1999);
result = false;
}
};
Assert.assertEquals(DateUtil.getDayNum(1999), 365);
}
@Test
public void testDayNotLeap() {
new Expectations(DateUtil.class) {
{
DateUtil.isLeapYear(1999);
result = true;
}
};
Assert.assertEquals(DateUtil.getDayNum(1999), 366);
}
看着似乎有点奇怪,不过这就是mock的作用,其实我们不需要关心闰年判断的逻辑,只需要关注闰年判断对我们新写的方法的结果的影响即可。