[TOC]
介绍
Junit4 是在 Junit3 基础上改进的单元测试框架
- junit4 不需要继承 TestCase
- 测试方法命名没有特定要求,只要在待测方法前加上@Test即可
- 通过
@befroe
替代@setUp
方法,@After
替代@tearDown
方法;
在一个测试类中,甚至可以使用多个 @Before 来注释多个方法,这些方法都是在每个测试之前运行
@Before 是在每个测试方法运行前均初始化一次,同理 @After 是在每个测试方法运行完毕后,均运行一次
也就是说,经过这两个注释的初始化和注销,可以保证各个测试方法之间的独立性而互不干扰,它的缺点是效率低
- 新增
@BeforeClass
和@AfterClass
使用这两个注释的方法,在该测试类中,的测试方法之前、后各运行一次,而不是按照各个方法各运行一次
对于一些资源消耗大的项目,可以使用这两个注释,减少 @Before @After 运行效率低的问题
- 新增测试类型
异常测试 @Test(expected=*.class)
和超时测试 @Test(timeout=xxx)
- 新增断言方法
新的assert方法 assertEquals(Object[] expected, Object[] actual)
,用于比较数组数据
测试用例写作注意事项
- 测试方法上面必须使用
@Test
注解进行修饰 - 测试方法必须使用
public void进行修饰,不能带有任何参数
- 测试单元中的每一个方法
必须独立测试,每个测试方法之间不能有依赖
- 新建一个源代码目录用来存放测试代码
测试类的包应该与被测试类的包保持一致
- 测试类使用Test做为类名的后缀(非必要)
- 测试方法使用test作为方法名的前缀(非必要)
- 尽量使用 assert 关键字来断言错误(非必要)
测试分析原则
- Failure 一般是单元测试使用的断言方法判断失败引起,说明预期结果和程序运行结果不一致
- Error 是有代码异常引起的,产生于测试代码本身中的Bug
- 测试用例
不是用来证明你是对的,而是用来证明你没有错
测试流程
@BeforeClass
public static void setUpBeforeClass() throws Exception {
}
@AfterClass
public static void setUpAfterClass() throws Exception {
}
@Before
public void before() throws Exception {
}
@After
public void after() throws Exception {
}
@BeforeClass
所修饰的方法在所有方法加载前执行,而且他是静态的在类加载后就会执行该方法,在内存中只有一份实例,适合用来加载配置文件
@AfterClass
所修饰的方法在所有方法执行完毕之后执行,通常用来进行资源清理,例如关闭数据库连接@Before
和@After
在每个测试方法执行前都会执行一次
JUnit注解活用
-
@Test(excepted=XX.class)
在运行时忽略某个异常 -
@Test(timeout=[ms])
允许程序运行的时间单位毫秒 -
@Ignore
所修饰的方法被测试器忽略 -
@RunWith
可以修改测试运行器org.junit.runner.Runne
单元测试的执行顺序
默认情况下,单元测试是无序的,单元测试框架的一个出发点是
单元性
,即每个单元之间互不影响,因此设置单元测试的执行顺序是没有意义的
不过如果涉及到特殊流程,或者非要让测试的执行顺序在自己的控制之下,也是可以做到的
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TempTest {
}
@FixMethodOrder
Junit 4.11 里增的指定测试方法执行顺序的特性,支持顺序有三种,这里是按名字排序
默认(MethodSorters.DEFAULT)
默认顺序由方法名hashcode值来决定,如果hash值大小一致,则按名字的字典顺序确定,由于hashcode的生成和操作系统相关,对于不同操作系统,可能会出现不一样的执行顺序,在某一操作系统上,多次执行的顺序不变
排序方法
/**
* DEFAULT sort order
* @type {Comparator}
*/
public static Comparator<Method> DEFAULT = new Comparator<Method>() {
public int compare(Method m1, Method m2) {
int i1 = m1.getName().hashCode();
int i2 = m2.getName().hashCode();
if (i1 != i2) {
return i1 < i2 ? -1 : 1;
}
return NAME_ASCENDING.compare(m1, m2);
}
};
按方法名(MethodSorters.NAME_ASCENDING)
按方法名称的进行排序,由于是按字符的字典顺序,所以以这种方式指定执行顺序会始终保持一致
需要对测试方法有一定的命名规则,如 测试方法均以 test_nnn 开头, nnn 代表 000-999 的数字
排序方法
/**
* Method name ascending lexicographic sort order, with {@link Method#toString()} as a tiebreaker
* @type {Comparator}
*/
public static Comparator<Method> NAME_ASCENDING = new Comparator<Method>() {
public int compare(Method m1, Method m2) {
final int comparison = m1.getName().compareTo(m2.getName());
if (comparison != 0) {
return comparison;
}
return m1.toString().compareTo(m2.toString());
}
};
JVM(MethodSorters.JVM)
按JVM返回的方法名的顺序执行,此种方式下测试方法的执行顺序是不可预测的,即每次运行的顺序可能都不一样
注意,JVM 返回的方法名在不同 jdk 上表现可能不一致,不过在 1.7 及以后的 jdk 返回的顺序肯定不一样
Junit4 测试方法顺序原理
实际上 Junit里是通过反射机制得到某个Junit里的所有测试方法,并生成一个方法的数组,然后依次执行数组里的这些测试方法;
而当用annotation指定了执行顺序,Junit在得到测试方法的数组后,会根据指定的顺序对数组里的方法进行排序
public static Method[] getDeclaredMethods(Class<?> clazz) {
Comparator<Method> comparator = getSorter(clazz.getAnnotation(FixMethodOrder.class)); //获取测试类指定的执行顺序
Method[] methods = clazz.getDeclaredMethods();
if (comparator != null) {
Arrays.sort(methods, comparator); //根据指定顺序排序
}
return methods;
}
定义的顺序为
package org.junit.runners;
/**
* Sort the methods into a specified execution order.
* Defines common {@link MethodSorter} implementations.
*
* @since 4.11
*/
public enum MethodSorters {
/**
* Sorts the test methods by the method name, in lexicographic order,
* with {@link Method#toString()} used as a tiebreaker
*/
NAME_ASCENDING(MethodSorter.NAME_ASCENDING),
/**
* Leaves the test methods in the order returned by the JVM.
* Note that the order from the JVM may vary from run to run
*/
JVM(null),
/**
* Sorts the test methods in a deterministic, but not predictable, order
*/
DEFAULT(MethodSorter.DEFAULT);
}
当设置为MethodSorters.JVM时,其并没有提供一个Comparator的实现
所以执行方法的顺序实际上就是 clazz.getDeclaredMethods();
得到的数组里方法的顺序
而由于java里对getDeclaredMethods返回的方法没有指定任何顺序,所以最终导致Junit测试方法的执行顺序也不是确定的
测试套件
测试套件是组织测试类一起运行的测试类
@RunWith(Suite.class)
@Suite.SuiteClasses({UserTest1,UserTest2,UserTest3})
public class SuiteTest{
}
测试套件注意事项
作为测试套件的入口类,类中不能包含任何方法
- 更改测试运行器Suite.class
- 将需要运行的测试类放入Suite.SuiteClasses({})的数组中
参数化设置
需要测试的仅仅是测试数据,代码结构是不变的,只需要更改测试入参
@RunWith(Parameterized.class)
public class ParameterTest {
int expected = 0;
int input1 = 0;
int input2 = 0;
@Parameters
public static Collection<Object[]> t() {
return Arrays.asList(new Object[][]{
{3,1,2},
{5,2,3}
});
}
public ParameterTest(int expected,int input1,int input2) {
this.expected = expected;
this.input1 = input1;
this.input2 = input2;
}
@Test
public void testAdd() {
assertEquals(expected, UserDao.add(input1,input2));
}
}
代码流程为
- 更改默认的测试运行器为
@RunWith(Parameterized.class)
- 声明变量来存放
预期值和测试值
例子中为 expected input1 input2 - 声明一个返回值为
Collection的公共静态方法,并用@Parameters修饰
- 为测试类声明一个带有参数的公共构造函数
ParameterTest
,并在其中为他声明变量赋值
单元测试统计与评测
单元测试代码覆盖介绍
首先,代码覆盖属于单元测试,集成测试不计算在其中
需要理解被测程序的逻辑,需要考虑到每个函数的输入与输出,逻辑分支代码的执行情况,这个时候我们的测试执行情况就以代码覆盖率来衡量,可以理解为白盒覆盖
这里介绍 java 代码的覆盖分类
- 行覆盖率:度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行
- 类覆盖率:度量计算class类文件是否被执行
- 分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的总分支数,确定执行和不执行的 分支数量
- 方法覆盖率:度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行
- 指令覆盖:计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全 独立源码格式
- 圈复杂度:在(线性)组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测试案例没有完全覆盖到这个模块
java 代码覆盖测试方法
代码生成二进制时使用的ASM技术修改字节码方法,可以修改Jar文件、class文件字节码文件,来注入源码,注入的过程做插桩,然后执行测试用例,统计是否调用
目前 java 使用的UAT和自动统计覆盖测试的技术
- maven 自动化构建链 http://maven.apache.org/ 其中 http://maven.apache.org/plugins/index.html 有 Junit 增强插件
- gradle 自动化构建链 https://gradle.org/ 其中 https://plugins.gradle.org/search?term=junit 有增强插件
- mockito 注入式单元测试 http://site.mockito.org/
- jacoco 覆盖率统计 http://www.eclemma.org/jacoco/