- 参考链接:
http://www.ibm.com/developerworks/cn/java/j-lo-junit4/index.html
https://blog.csdn.net/Zen99T/article/details/50572373
https://tonydeng.github.io/2016/05/11/junit-more-feature/
- 案例学习:
- 图书推荐:
《Java测试驱动开发》
- 自己写的测试代码供参考:
https://github.com/zhangwanyue/MockitoJUnitTest
隔离
JUnit中每个测试方法都是在独立的类实例中执行的,因此只要没有使用全局变量和外部资源(如数据库和API),这些测试都将是彼此隔离的,即不管一个测试做什么,都不会影响其他测试。
基本测试方法
测试方法需要按照一定的规范书写:
测试方法必须使用注解 org.junit.Test 修饰。
测试方法必须使用 public void 修饰,而且不能带有任何参数。
例子:
public class TestWordDealUtil {
……
// 测试 null 时的处理情况
@Test public void wordFormat4DBNull(){
String target = null;
String result = WordDealUtil.wordFormat4DB(target);
assertNull(result);
}
// 测试空字符串的处理情况
@Test public void wordFormat4DBEmpty(){
String target = "";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("", result);
}
// 测试当首字母大写时的情况
@Test public void wordFormat4DBegin(){
String target = "EmployeeInfo";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_info", result);
}
// 测试当尾字母为大写时的情况
@Test public void wordFormat4DBEnd(){
String target = "employeeInfoA";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_info_a", result);
}
// 测试多个相连字母大写时的情况
@Test public void wordFormat4DBTogether(){
String target = "employeeAInfo";
String result = WordDealUtil.wordFormat4DB(target);
assertEquals("employee_a_info", result);
}
}
Fixture
- 方法级别Fixture
// 初始化 Fixture 方法
@Before public void init(){ …… }
// 注销 Fixture 方法
@After public void destroy(){ …… }
- 类级别Fixture
// 类级别 Fixture 初始化方法
@BeforeClass public static void dbInit(){ …… }
// 类级别 Fixture 注销方法
@AfterClass public static void dbClose(){ …… }
异常及时间测试
注解 org.junit.Test 中有两个非常有用的参数:expected 和 timeout。参数 expected 代表测试方法期望抛出指定的异常,如果运行测试并没有抛出这个异常,则 JUnit 会认为这个测试没有通过。这为验证被测试方法在错误的情况下是否会抛出预定的异常提供了便利。举例来说,方法 supportDBChecker 用于检查用户使用的数据库版本是否在系统的支持的范围之内,如果用户使用了不被支持的数据库版本,则会抛出运行时异常 UnsupportedDBVersionException。测试方法 supportDBChecker 在数据库版本不支持时是否会抛出指定异常的单元测试方法大体如下:
@Test(expected=UnsupportedDBVersionException.class)
public void unsupportedDBCheck(){
……
}
注解 org.junit.Test 的另一个参数 timeout,指定被测试方法被允许运行的最长时间应该是多少,如果测试方法运行时间超过了指定的毫秒数,则 JUnit 认为测试失败。这个参数对于性能测试有一定的帮助。例如,如果解析一份自定义的 XML 文档花费了多于 1 秒的时间,就需要重新考虑 XML 结构的设计,那单元测试方法可以这样来写:
@Test(timeout=1000)
public void selfXMLReader(){
……
}
忽略测试方法
JUnit 提供注解 org.junit.Ignore 用于暂时忽略某个测试方法,因为有时候由于测试环境受限,并不能保证每一个测试方法都能正确运行。例如下面的代码便表示由于没有了数据库链接,提示 JUnit 忽略测试方法 unsupportedDBCheck:
@Ignore(“db is down”)
@Test(expected=UnsupportedDBVersionException.class)
public void unsupportedDBCheck(){
……
}
但是一定要小心。注解 org.junit.Ignore 只能用于暂时的忽略测试,如果需要永远忽略这些测试,一定要确认被测试代码不再需要这些测试方法,以免忽略必要的测试点。
测试运行器
JUnit 中所有的测试方法都是由测试运行器负责执行的。JUnit 为单元测试提供了默认的测试运行器,但 JUnit 并没有限制您必须使用默认的运行器。相反,您不仅可以定制自己的运行器(所有的运行器都继承自 org.junit.runner.Runner),而且还可以为每一个测试类指定使用某个具体的运行器。指定方法也很简单,使用注解 org.junit.runner.RunWith
在测试类上显式的声明要使用的运行器即可:
@RunWith(CustomTestRunner.class)
public class TestWordDealUtil {
……
}
显而易见,如果测试类没有显式的声明使用哪一个测试运行器,JUnit 会启动默认的测试运行器执行测试类(比如上面提及的单元测试代码)。一般情况下,默认测试运行器可以应对绝大多数的单元测试要求;当使用 JUnit 提供的一些高级特性(例如即将介绍的两个特性)或者针对特殊需求定制 JUnit 测试方式时,显式的声明测试运行器就必不可少了。
测试套件
在实际项目中,随着项目进度的开展,单元测试类会越来越多,可是直到现在我们还只会一个一个的单独运行测试类,这在实际项目实践中肯定是不可行的。为了解决这个问题,JUnit 提供了一种批量运行测试类的方法,叫做测试套件。这样,每次需要验证系统功能正确性时,只执行一个或几个测试套件便可以了。测试套件的写法非常简单,您只需要遵循以下规则:
- 创建一个空类作为测试套件的入口。
- 使用注解 org.junit.runner.RunWith 和 org.junit.runners.Suite.SuiteClasses 修饰这个空类。
- 将 org.junit.runners.Suite 作为参数传入注解 RunWith,以提示 JUnit 为此类使用套件运行器执行。
- 将需要放入此测试套件的测试类组成数组作为注解 SuiteClasses 的参数。
- 保证这个空类使用 public 修饰,而且存在公开的不带有任何参数的构造函数。
package com.ai92.cooljunit;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
……
/**
* 批量测试 工具包 中测试类
* @author Ai92
*/
@RunWith(Suite.class)
@Suite.SuiteClasses({TestWordDealUtil.class})
public class RunAllUtilTestsSuite {
}
上例代码中,我们将前文提到的测试类 TestWordDealUtil 放入了测试套件 RunAllUtilTestsSuite 中,在 Idea 中运行测试套件,就可以看到测试类 TestWordDealUtil 被调用执行了。测试套件中不仅可以包含基本的测试类,而且可以包含其它的测试套件,这样可以很方便的分层管理不同模块的单元测试代码。但是,您一定要保证测试套件之间没有循环包含关系,否则无尽的循环就会出现在您的面前……。
参数化测试
为了保证单元测试的严谨性,我们需要模拟不同类型的字符串来测试方法的处理能力,为此我们会编写大量的单元测试方法。可是这些测试方法都是大同小异:代码结构都是相同的,不同的仅仅是测试数据和期望值。有没有更好的方法将测试方法中相同的代码结构提取出来,提高代码的重用度,减少复制粘贴代码的烦恼?可以使用 JUnit 提供的参数化测试方式应对这个问题:
- 为准备使用参数化测试的测试类指定特殊的运行器 org.junit.runners.Parameterized。
- 为测试类声明几个变量,分别用于存放期望值和测试所用数据。
- 为测试类声明一个使用注解 org.junit.runners.Parameterized.Parameters 修饰的,返回值为 java.util.Collection 的公共静态方法,并在此方法中初始化所有需要测试的参数对。
- 为测试类声明一个带有参数的公共构造函数,并在其中为第二个环节中声明的几个变量赋值。
- 编写测试方法,使用定义的变量作为参数进行测试。 我们按照这个标准,重新改造一番我们的单元测试代码:
package com.ai92.cooljunit;
import static org.junit.Assert.assertEquals;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
@RunWith(Parameterized.class)
public class TestWordDealUtilWithParam {
private String expected;
private String target;
@Parameters
public static Collection words(){
return Arrays.asList(new Object[][]{
{"employee_info", "employeeInfo"}, // 测试一般的处理情况
{null, null}, // 测试 null 时的处理情况
{"", ""}, // 测试空字符串时的处理情况
{"employee_info", "EmployeeInfo"}, // 测试当首字母大写时的情况
{"employee_info_a", "employeeInfoA"}, // 测试当尾字母为大写时的情况
{"employee_a_info", "employeeAInfo"} // 测试多个相连字母大写时的情况
});
}
/**
* 参数化测试必须的构造函数
* @param expected 期望的测试结果,对应参数集中的第一个参数
* @param target 测试数据,对应参数集中的第二个参数
*/
public TestWordDealUtilWithParam(String expected , String target){
this.expected = expected;
this.target = target;
}
/**
* 测试将 Java 对象名称到数据库名称的转换
*/
@Test public void wordFormat4DB(){
assertEquals(expected, WordDealUtil.wordFormat4DB(target));
}
}
很明显,代码瘦身了。在静态方法 words 中,我们使用二维数组来构建测试所需要的参数列表,其中每个数组中的元素的放置顺序并没有什么要求,只要和构造函数中的顺序保持一致就可以了。现在如果再增加一种测试情况,只需要在静态方法 words 中添加相应的数组即可,不再需要复制粘贴出一个新的方法出来了。
AssertThat & JUnitMatchers
- 参考链接:
https://github.com/junit-team/junit4/wiki/Matchers-and-assertThat
https://junit.org/junit4/javadoc/latest/org/junit/matchers/JUnitMatchers.html