单元测试:JUnit4学习笔记 - 1
导读
单元测试是项目成功不可或缺的部分,是一种产生鲁棒性代码的廉价、简单、高效的方法。如果这篇文章有幸得到了您的阅读,那么想必您已经知道了单元测试的必要性并且迫切的想知道如何进行单元测试。本篇博文是笔者(KinoamyFx)对于JUnit4学习笔记,内容会涉及到单元测试的一些概念和JUnit4的初级使用方法,如能解决您的疑惑,是Kino的荣幸,以后也请多多指教。
一、下载与安装
点击进入JUnit - Github安装引导页,当前最新版本为4.12。
Maven依赖如下:
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
Gradle依赖如下:
apply plugin: 'java'
dependencies {
testCompile 'junit:junit:4.12'
}
基本语法
注解 - Annotation
测试类运行流程
下面使用简单的打印Hello World做示例:
/**
* 路径是 src/main/java/cn/kinoamyfx/junit/HelloWorld.java
* 算法的源码,普通类含有一个普通方法.
*/
public class HelloWorld {
public String sayHello() {
return "Hello World!";
}
}
/**
* 路径是test/java/cn/kinoamyfx/junit/HelloWorldTest.java
* 测试类
*/
public class HelloWorldTest {
/**
* 使用BeforeClass注解的方法仅在测试类初始化的时候运行一次,可以在此进行测试类私有变量的初始化操作,
* 比如数据库连接池这种比较消耗资源但又可以重复使用的对象。
*/
@BeforeClass
public static void beforceClass() throws Exception {
}
/**
* 使用Before注解的方法会在所有的使用Test注解的方法前执行一次,
* 在本例中会在testSayHello和testSayHelloExcepted方法执行前执行,共两次。
*/
@Before
public void before() throws Exception {
}
/**
* 使用Test注解的是一般的测试方法.本方法未指定expected和timeout.
*/
@Test
public void testSayHello() throws Exception {
new HelloWorld().sayHello();
}
/**
* 本方法指定了期望的抛出异常和超时运行时间.
*/
@Test(timeout = 1000, expected = IOException.class)
public void testSayHelloExcepted() throws Exception {
}
/**
* 使用After注解的方法类似Beforce,在测试方法执行后执行,本例中执行两次.
*/
@After
public void after() throws Exception {
}
/**
* 使用AfterClass注解的方法类似BeforeClass,可以在此进行对象的关闭操作.
*/
@AfterClass
public static void afterClass() throws Exception {
}
}
由源代码可以看到JUnit4使用Java5引入的注解来进行标记,然后JUnit4内部通过反射获取并执行方法,使得JUnit4的语法更加简洁易用,提高了开发效率。
如果有童鞋还使用主函数来调试程序,那么务必请使用单元测试来调试你的程序。
下面给出单元测试类的执行流程:
参数化测试@Parameters - 使用构造函数传进数据并多次自动进行测试
/**
* 1.我们需要通过Parameterized.class来执行这个测试类.
*/
@RunWith(Parameterized.class)
public class HelloWorldTest {
private int numberA;
private int numberB;
private static int count=0;
/**
* 2. 我们需要使用@Parameters注解来指定传入数据的方法.
*/
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(
new Object[][]{{0, 0}, {1, 1}, {2, 1}, {3, 2}, {4, 3}, {5, 5}, {6, 8}}
);
}
/**
* 3. 编写构造函数,初始化传入值.
* 本例中实际上依次生成了7个HelloWorldTest对象.每个对象构造函数传入值不同.
*/
public HelloWorldTest(int numberA, int numberB) {
this.numberA = numberA;
this.numberB = numberB;
System.out.println(++count);
}
/**
* 4. 在测试方法中使用传入值.
*/
@Test
public void handleParameters() {
System.out.println("A:"+numberA+"B:"+numberB);
}
}
理论机制@Theory - 增强型参数化测试
在参数化测试中,我们需要四个步骤来传入数据测试,操作十分麻烦。
在理论机制中提供了定义数据集的注解 - @DataPoint和@DataPoints,两者都可以注解在变量上或者数据的函数上。
@DataPoint 表示单组数据,如[1,2]
@DataPoints 表示多组数据,如{ [1,2] , [2,5] }
其用法如下:
/**
* 通过Theories.class来执行这个测试类.
*/
@RunWith(Theories.class)
public class HelloWorldTest {
@DataPoint
public static final int Data1 = 1;
@DataPoints
public static final int[] DataSet1 = new int[]{11, 12};
@Theory
public void testTheory(int data1) throws Exception {
System.out.println(data1);
}
}
运行结果依次为:
1
11
12
无论是单个数据还是数据集都被合并成一个数据集然后依次传递给测试函数。
在传入多个参数的情况下:
@Theory
public void testTheory(int data1,int data2) throws Exception {
System.out.println(data1+":"+data2);
}
答案为:
1:1
1:11
1:12
11:1
11:11
11:12
12:1
12:11
12:12
多个参数的情况下,传入的数据集做了笛卡尔积。
在多个参数集(参数集类型不同)的情况下,此特性可以产生意想不到的效果。
我们可以简化参数集的描写,直接写在参数列表中:
@RunWith(Theories.class)
public class HelloWorldTest {
@Theory
public void testTheory(@TestedOn(ints = {1,11,12}) int data1) throws Exception {
System.out.println(data1);
}
}
简化多数据集的描写:
@RunWith(Theories.class)
public class HelloWorldTest {
@Theory
public void testTheory(
@TestedOn(ints = {1,11,12}) int data1,
@TestedOn(ints = {2,21,22}) int data2
) throws Exception {
System.out.println(data1+":"+data2);
}
}
关于自定义TestedOn的方法,请查看Wiki,Kino就不赘述了.
JUnit4 Wiki - Theories
控制测试函数运行顺序 - 使用@FixMethodOrder调整函数运行顺序
@MethodSorters详细解释
注意: 此注解仅在JUnit4-4.11及更好版本才可以使用.
/**
* 控制测试函数执行顺序有三种规则.
*/
//@FixMethodOrder(MethodSorters.DEFAULT)
//@FixMethodOrder(MethodSorters.JVM)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class HelloWorldTest {
}
MethodSorters.DEFAULT:默认顺序由方法名hashcode值来决定,如果hash值大小一致,则按名字的字典顺序确定
由于hashcode的生成和操作系统相关,所以对于不同操作系统,可能会出现不一样的执行顺序。MethodSorters.JVM:按JVM返回的方法名的顺序执行,此种方式下测试方法的执行顺序是不可预测的,即使在同一操作系统每次运行的顺序可能都不一样。
MethodSorters.NAME_ASCENDING:按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致。不过这种方式需要对测试方法有一定的命名规则,如测试方法均以testNNN开头(NNN表示测试方法序列号 001-999)
函数规则@Rule - 使用TestRule进行规则控制
Rule有两种分别是:@ClassRule和@Rule,可以注释在实现了TestRule接口的变量上或者返回TestRule类型的方法上。而TestRule则提供了验证、监视TestCase和外部资源管理等能力。
简单的说就是提供了测试用例执行过程中一些通用功能的共享的能力,使我们不必重复编写一些功能类似的代码。(我们把子类共有的方法重构到父类中去也是类似思想)
下面是常用的Rule:
- Verifier: 验证测试执行结果的正确性。
- ErrorCollector: 收集测试方法中出现的错误信息,测试不会中断,如果有错误发生测试结束后会标记失败。
- ExpectedException: 提供灵活的异常验证功能。
- Timeout: 用于测试超时的Rule。
- ExternalResource: 外部资源管理。
- TemporaryFolder: 在JUnit的测试执行前后,创建和删除新的临时目录。
- TestWatcher: 监视测试方法生命周期的各个阶段。
- TestName: 在测试方法执行过程中提供获取测试名字的能力
详细使用方法需要更加细致的自定义,所以这里给个传送门,有兴趣的读者可以点击查看:
JUnit4 - Rule机制详解
断言 - Assume and Assert
断言需要在@Test注解的测试方法中使用,目的是检测期望的值与运行得到的值是否一致,如果不一致则判定该测试失败。
旧式断言
@Test
public void testSayHello() {
Assert.assertEquals("如果不相等,打印此字符串", "Hello World!", new HelloWorld().sayHello());
//旧式断言有很多函数可供使用,下面列出常用函数名
//Assert.assertNotEquals();
//Assert.assertArrayEquals();
//Assert.assertFalse();
//Assert.assertTrue();
//Assert.assertSame();
//Assert.assertNotSame();
//Assert.assertNull();
//Assert.assertNotNull();
//还有一种类似旧式断言的假设,其用法与断言一样,不再赘述.
//Assume.assumeTrue();
//Assume.assumeFalse();
//Assume.assumeNoException();
//Assume.assumeNotNull();
//Assume.assumeThat();
//新式断言
//Assert.assertThat();
}
新式断言
除了上述断言以外,JUnit4还提供一个新断言。结合hamcrest-core库可以写出非常复合我们自然语言的断言声明。
@Test
public void testSayHello() {
String helloWorld = new HelloWorld().sayHello();
Assert.assertThat(helloWorld, CoreMatchers.is("Hello World!"));
}
我们可以把CoreMatchers.is()方法静态导入,那么就可以使用缩略写法
Assert.assertThat(helloWorld, is("Hello World!"));
我们可以使用各种断言条件组合,如下:
@Test
public void testSayHello() {
String s = new HelloWorld().sayHello();
// s是Hello World!
Assert.assertThat(s, is("Hello World!"));
// s包含字符串Hello
Assert.assertThat(s, containsString("Hello"));
// s满足所有条件
Assert.assertThat(s, allOf(containsString("Hello"), containsString("World")));
// s满足任一条件
Assert.assertThat(s, anyOf(containsString("Hello"), containsString("China")));
}
Kino这里推荐大家都尽量使用新断言,可以增强代码的阅读性,毕竟代码是给人读的。
Mock对象 - 使用构造对象代替难以生成的对象
Mock技术和Stub都是用来模拟难以生成的对象实例的,如Servlet.
关于Mock详细的技术,Kino会在接下来的博文中继续记下学习笔记,敬请期待!
这里给出一些可供读者参考的文章,可解燃眉之急:
什么是Stub?什么是Mock
使用PowerMock进行Mock测试
结尾 - 感谢您阅读我的文章
这篇博文是Kino初步接触单元测试框架JUnit4而产生的学习笔记,对JUnit4的基础用法做了比较浅显的讲解,若有错误,请批评指出,您的评价是给Kino最好的礼物。
在接下来的单元测试文章中,会依次学习到一些单元测试的准则和其他关于单元测试的框架,不过要等到Kino有空的时候才能来写了。
最后再次感谢您的阅读,希望有所收获!