文章目录
一、概述
单元测试(Unit Testing)是指对软件中的最小可测试单元进行检查和验证,一般要根据实际情况去判定其具体含义,比如在 Java 语言中,单元可以指一个类或一个方法。JUnit 是一个 Java 语言简单、开源的单元测试框架,在 Java 开发中比较流行且常用的测试框架,希望读者能通过这篇文章来深入了解 JUnit 的特性与使用。
该文章使用的版本为 JUnit4
二、JUnit 基本用法
1. 测试运行器
JUnit 测试的运行器可以指定当前测试通过怎样的方式去运行,也可以配置运行的上下文,可以在类或父类上使用 @RunWith
注解来指定一个特定的运行器,而不是使用 JUnit 默认的运行器,很多第三方框架整合 JUnit 框架,一般都会有自定义的 Runner
接口实现。
@RunWith(JUnit4.class)
public class DemoTest {
@Test
public void assertTest() {
// do something
}
}
下面是 JUnit 专门提供的运行器,在后面我们会详细讲解到它们的用法:
Suite
:可以手动构建多个类的测试套件的标准运行器Parameterized
:可以为测试方法以参数的形式提供测试数据的标准运行器Categories
:可以手动指定一组测试,配置包含或不包含一些测试方法或类的运行器
2. 断言与假设
2.1 断言
Java 提供了关键字 assert
来实现断言,断言功能可以通过 Java 运行参数来启用或者禁用,当断言判断失败时,会抛出 AssertionError
错误,它的用法如下:
assert condition;
assert condition : message;
JUnit 为所有基本类型、对象和数组类型提供了一组断言方法,这些方法都位于 Assert
类中,当断言判断失败时,会抛出 AssertionError
错误,示例代码如下:
public class DemoAssertTest {
@Test
public void assertTest() {
int a = 5;
assert a < 0;
assert a < 0 : "整数为正数";
}
@Test
public void assertJUnitTest() {
int a = 2;
Object expected = new Object();
Object actual = new Object();
assertTrue("条件为假", a > 1);
assertFalse("条件为真", a > 4);
assertEquals("整数与期望的数不相等", 100, 80);
assertNotEquals("整数与期望的数相等", 100, 80);
assertArrayEquals("", new Integer[]{1, 2, 3, 4, 5}, new Integer[]{1, 2, 3, 4, 5});
assertNotNull("", actual);
assertNull("", actual);
assertSame("", expected, actual);
assertNotSame("", expected, actual);
assertThat(actual, is(expected));
}
}
断言方法的错误提示消息参数都是可选的,一般断言方法的参数是布尔类型,来进行条件判断,也有判断实际值与期望值是否相符的。期望值不一定是确定的值,例如在 assertThat
方法中,参数 Matcher
接口提供了一个匹配的操作,用来判断实际值是否符合期望。
2.2 假设
假设与断言类似,它一般用于测试代码之前,判断测试的参数和执行环境条件,可以避免一些不关注的点来影响测试结果,Assume
类提供了与断言类似的方法,当假设判断失败时,会抛出 AssumptionViolatedException
异常,示例代码如下:
public class DemoAssumeTest {
@Test public void filenameIncludesUsername() {
// 假设前提条件
assumeThat(File.separatorChar, is('/'));
assertThat(new User("optimus").configFileName(), is("configfiles/optimus.cfg"));
}
@Test public void correctBehaviorWhenFilenameIsNull() {
// 假设前提条件
assumeTrue(bugFixed("13356")); // bugFixed is not included in JUnit
assertThat(parse(null), is(new NullDocument()));
}
@Test
public void test() {
int a = 2;
Object object = new Object();
assumeTrue("Assume true", true);
assumeFalse("Assume false", false);
assumeNotNull(object);
assumeNoException("Assume no exception", new RuntimeException());
assumeThat("Assume that", a, is(3));
}
}
断言方法的错误提示消息参数都是可选的,与断言的方法参数类似,例如在 assertThat
方法中,参数 Matcher
接口提供了一个匹配的操作,用来判断实际值是否符合期望。
3. 套件测试
套件测试可以手动构建多个测试类一起运行,它使用 Suite
运行器来实现,假设我们存在如下三个测试类:
public class Test1 {
@Test
public void test() {
System.out.println("Test1.test");
}
@Test
public void test2() {
System.out.println("Test1.test2");
}
}
public class Test2 {
@Test
public void test() {
System.out.println("Test2.test");
}
@Test
public void test2() {
System.out.println("Test2.test2");
}
}
public class Test3 {
@Test
public void test() {
System.out.println("Test3.test");
}
}
新建一个测试类并使用 @RunWith(Suite.class)
注解来指定运行器,然后使用 @SuiteClasses(TestClass1.class, ...)
注解来指定包含的测试类,运行此类就会运行套件包含的所有测试类。
@RunWith(Suite.class)
@Suite.SuiteClasses({Test1.class, Test2.class, Test3.class})
public class DemoSuiteTest {
}
4. 分类测试
分类测试可以指定将某些测试类或测试方法进行分类,然后运行其中的某些类别的测试方法,或者不运行其中的某些类别的测试方法,它使用 Categories
运行器来实现,首先我们定义两个接口来标记类别:
public interface FastTests {
}
public interface SlowTests {
}
再定义两个测试类示例,使用 @Category
注解来标记测试类或测试方法的分类:
public class Test1 {
@Test
public void test() {
System.out.println("Test1.test");
}
@Test
@Category(FastTests.class)
public void test2() {
System.out.println("Test1.test2");
}
}
@Category({FastTests.class, SlowTests.class})
public class Test2 {
@Test
public void test() {
System.out.println("Test2.test");
}
@Test
public void test2() {
System.out.println("Test2.test2");
}
}
分类测试是套件测试的扩展,运行器类也是其子类,所以我们在运行分类测试的时候,首先得使用 @Suite.SuiteClasses
注解来指定需要分类的所有测试类,然后使用 @RunWith(Categories.class)
来指定分类测试运行器,@Categories.IncludeCategory
注解指定运行的类别,@Categories.ExcludeCategory
指定不运行的类别,示例如下:
@RunWith(Categories.class)
@Categories.IncludeCategory(FastTests.class)
@Categories.ExcludeCategory(SlowTests.class)
@Suite.SuiteClasses({Test1.class, Test2.class, Test3.class})
public class DemoCategoriesTest {
}
5. 顺序测试
JUnit 支持指定测试方法的执行顺序,在一个测试类上,可以使用 @FixMethodOrder
注解来指定,通过选择不同的方法 Comparator
接口实现来选择执行顺序。
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class DemoOrderTest {
@Test
public void testB() {
System.out.println("DemoOrderTest.testB");
}
@Test
public void testA() {
System.out.println("DemoOrderTest.testA");
}
@Test
public void testC() {
System.out.println("DemoOrderTest.testC");
}
}
6. 忽略测试
在方法上面使用 @Ignore
来指定忽略的测试方法,也可以指明忽略的理由。
public class DemoIgnoreTest {
@Test
@Ignore("This test method is ignored.")
public void test() {
System.out.println("DemoIgnoreTest.test");
}
@Test
public void test2() {
System.out.println("DemoIgnoreTest.test2");
}
}
7. 超时测试
在测试方法上的 @Test
注解上添加 timeout
属性可以指定该测试方法的执行超时时间,如果测试方法执行的耗时超过了该时间,则会抛出 TestTimedOutException
异常。
public class DemoTimeoutTest {
@Test(timeout = 100)
public void test() throws InterruptedException {
TimeUnit.SECONDS.sleep(3);
System.out.println("DemoTimeoutTest.test");
}
}
8. 参数测试
在测试的过程中,我们经常会给定一些测试数据,来跑单元测试方法,这在测试场景中非常有用。参数测试使用 Parameterized
运行器来实现,首先定义一个静态方法,并且使用 @Parameters
注解,静态方法的返回值是数组或者列表,然后对应数据在测试类上定义字段,使用构造方法来注入测试数据。
@RunWith(Parameterized.class)
public class DemoParameterTest {
private int input1;
private int input2;
public DemoParameterTest(int input1, int input2) {
this.input1 = input1;
this.input2 = input2;
}
@Parameters
public static Integer[][] data() {
return new Integer[][]{
{1, 11}, {2, 22}, {3, 33}, {4, 44}, {5, 55}
};
}
@Test
public void test() {
System.out.println("DemoParameterTest.test::" + input1 + "-" + input2);
}
}
除了使用构造方法外,还可以在字段上面使用 @Parameter
注解来注入测试数据,在注解的属性中指定数据列与字段的对应,注意字段的访问权限必须是 public
。
@RunWith(Parameterized.class)
public class DemoParameterTest {
@Parameter
public int input1;
@Parameter(1)
public int input2;
@Parameters
public static Integer[][] data() {
return new Integer[][]{
{1, 11}, {2, 22}, {3, 33}, {4, 44}, {5, 55}
};
}
@Test
public void test() {
System.out.println("DemoParameterTest.test::" + input1 + "-" + input2);
}
}
有多少组数据,测试方法就会执行多少次,为了区分每个测试数据执行的展示结果,可以在 @Parameters
注解上使用 name
属性来指定测试结果命名,命名的规则如下:
{index}
:当前参数的索引{0}, {1}, …
:第1个,第2个等等的参数值
@Parameters(name = "{index}:{0}-{1}")
public static Integer[][] data() {
return new Integer[][]{
{1, 11}, {2, 22}, {3, 33}, {4, 44}, {5, 55}
};
}
例如上面示例在 IDEA 中执行的结果如下:
9. 理论测试
和参数测试类似,我们也可以使用理论测试来指定测试数据。理论测试使用 Theories
运行器来实现,可以在测试类中定义常量字段来指定测试数据,在字段上使用 @DataPoint
注解来标记,然后在测试方法定义和字段类型一致的参数就能注入数据。
@RunWith(Theories.class)
public class DemoTheoriesTest {
@DataPoint
public static String GOOD_USERNAME = "CodeArtist";
@DataPoint
public static String USERNAME_WITH_SLASH = "Hello CodeArtist";
@Theory
public void test(String str) {
System.out.println("DemoTheoriesTest.test::" + str);
}
}
也可以直接在测试方法的参数上使用 @TestedOn
来指定数据,如果有多个参数,会运行参数组合的每一种情况。
@RunWith(Theories.class)
public class DemoTheoriesTest {
@Theory
public void test(@TestedOn(ints = {1, 2, 3}) int input1,
@TestedOn(ints = {11, 22, 33}) int input2) {
System.out.println("DemoTheoriesTest.test::" + input1 + "-" + input2);
}
}
除了前面的方法指定数据外,JUnit 也支持自定义注解来指定数据。自定义一个注解如下:
@Target(PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@ParametersSuppliedBy(BetweenSupplier.class)
public @interface Between {
int first();
int last();
}
实现 ParameterSupplier
接口编写自定义注解的规则实现如下:
public class BetweenSupplier extends ParameterSupplier {
@Override
public List<PotentialAssignment> getValueSources(ParameterSignature sig) throws Throwable {
List<PotentialAssignment> list = new ArrayList<>();
Between between = sig.getAnnotation(Between.class);
for (int i = between.first(); i <= between.last(); i++) {
list.add(PotentialAssignment.forValue("between", i));
}
return list;
}
}
使用自定义的注解来指定参数,也会运行所有参数组合的情况。
@RunWith(Theories.class)
public class DemoTheoriesTest {
@Theory
public void test(@Between(first = 1, last = 3) int input1,
@Between(first = 4, last = 6) int input2) {
System.out.println("DemoTheoriesTest.test::" + input1 + "-" + input2);
}
}
10. 测试规则
规则可以为测试类中的每个测试方法灵活地添加扩展或重定义测试方法的行为,开发人员可以使用 JUnit 内置规则或自定义测试规则,方法级规则在规则字段上使用 @Rule
注解来指定,类级规则在规则字段上使用 @ClassRule
来指定。
10.1 内置规则
ExternalResource
ExternalResource
规则类是一个抽象类,可以为测试环境提供外部资源的访问,比如在测试前建立一个访问(文件、套接字、服务、数据库连接等等),在测试后关闭一个访问。
public class DemoExternalResourceTest {
@Rule
public final ExternalResource externalResource = new ExternalResource() {
@Override
protected void before() {
System.out.println("DemoExternalResourceTest.before");
}
@Override
protected void after() {
System.out.println("DemoExternalResourceTest.after");
}
};
@Test
public void test() {
System.out.println("DemoExternalResourceTest.test");
}
}
TemporaryFolder
TemporaryFolder
规则继承了 ExternalResource
抽象类,实现了在测试过程中创建临时文件或文件夹的功能,在测试过程中创建的文件都会在测试后自动删除。
public class DemoTemporaryFolderTest {
@Rule
public final TemporaryFolder folder = new TemporaryFolder();
@Test
public void test() throws IOException {
File createdFile = folder.newFile("codeartist.txt");
File createdFolder = folder.newFolder("CodeArtist");
System.out.println("DemoTemporaryFolderTest.test#createdFile: " + createdFile);
System.out.println("DemoTemporaryFolderTest.test#createdFolder: " + createdFolder);
}
}
Verifier
Verifier
规则类是一个抽象类,它可以在测试方法运行完后执行一些逻辑,所以可以用来校验测试的结果。
public class DemoVerifierTest {
private static String result = "";
@Rule
public final Verifier verifier = new Verifier() {
@Override
protected void verify() {
result += "verify";
}
};
@Test
public void test() {
System.out.println("DemoVerifierTest.test::" + result);
}
@Test
public void test2() {
System.out.println("DemoVerifierTest.test::" + result);
}
}
ErrorCollector
ErrorCollector
规则继承了 Verifier
抽象类,它是一个错误收集器,在测试方法运行时,遇到第一个异常会继续执行,先收集所有的异常然后一次性报告异常信息。
public class DemoErrorCollectorTest {
@Rule
public final ErrorCollector collector = new ErrorCollector();
@Test
public void test() {
try {
System.out.println("DemoErrorCollectorTest.test::First exception");
int a = 1 / 0;
} catch (Exception e) {
collector.addError(e);
}
try {
System.out.println("DemoErrorCollectorTest.test::Second exception");
String a = null;
System.out.println(a.getBytes());
} catch (Exception e) {
collector.addError(e);
}
}
}
TestWatcher
TestWatcher
规则类是一个抽象类,它可以监视一个测试方法执行的生命周期动作,但不能修改,例如可以在测试开始时、成功执行时、执行结束时添加逻辑。
public class DemoTestWatcherTest {
@Rule
public final TestWatcher watcher = new TestWatcher() {
@Override
protected void starting(Description description) {
System.out.println("DemoTestWatcherTest.starting");
}
@Override
protected void succeeded(Description description) {
System.out.println("DemoTestWatcherTest.succeeded");
}
@Override
protected void finished(Description description) {
System.out.println("DemoTestWatcherTest.finished");
}
};
@Test
public void test() {
System.out.println("DemoTestWatcherTest.test");
}
}
TestName
TestName
规则可以获取当前运行的测试方法的名称。
public class DemoTestNameTest {
@Rule
public final TestName testName = new TestName();
@Test
public void test() {
System.out.println("DemoTestNameTest.test::" + testName.getMethodName());
}
}
Timeout
Timeout
规则可以指定测试类中所有测试方法的超时时间。
public class DemoTimeoutTest {
@Rule
public final Timeout timeout = Timeout.seconds(1);
@Test
public void test() throws InterruptedException {
TimeUnit.SECONDS.sleep(2);
System.out.println("DemoTimeoutTest.test");
}
}
ExpectedException
ExpectedException
规则可以指定测试方法抛出预期的异常或存在预期的异常消息描述,如果没有达到预期会抛出 AssertionError
。
public class DemoExpectedExceptionTest {
@Rule
public final ExpectedException exception = ExpectedException.none();
@Test
public void test() {
exception.expect(NullPointerException.class);
exception.expectMessage("happened?");
exception.expectMessage(startsWith("What"));
throw new NullPointerException("What happened?");
}
}
ClassRule
@ClassRule
注解扩展了 @Rule
的方法级规则,在定义的规则字段上需要添加 static
修饰,类级别规则在测试类运行时执行,与测试方法无关。
public class DemoClassRuleTest {
@ClassRule
public static final ExternalResource r = new ExternalResource() {
@Override
protected void before() {
System.out.println("DemoClassRuleTest.before");
}
@Override
protected void after() {
System.out.println("DemoClassRuleTest.after");
}
};
@Test
public void test() {
System.out.println("DemoClassRuleTest.test");
}
@Test
public void test2() {
System.out.println("DemoClassRuleTest.test2");
}
}
RuleChain
RuleChain
规则可以定义规则链,也就是说可以在当前测试类中定义多个规则。
public class DemoRuleChainTest {
@Rule
public final RuleChain chain = RuleChain
.outerRule(new TestLogger("outer rule"))
.around(new TestLogger("around1 rule"))
.around(new TestLogger("around2 rule"));
@Test
public void test() {
System.out.println("DemoRuleChainTest.test");
}
}
10.2 自定义规则
我们通过实现 TestRule
接口来自定义规则,也可以继承内置的基础规则抽象类来实现。
public class TestLogger implements TestRule {
private final String message;
public TestLogger(String message) {
this.message = message;
}
@Override
public Statement apply(Statement base, Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
System.out.println("TestLogger.evaluate::" + message);
base.evaluate();
}
};
}
}
11. 测试卡具
测试卡具可以在测试方法执行的前面或者后面执行一些逻辑,@Before
注解修饰的方法在所有测试方法执行前都会执行,相反 @After
注解修饰的方法在所有测试方法执行后都会执行,它们是方法级别的,而 @BeforeClass
和 @AfterClass
注解修饰的方法是类级别的,方法必须添加 static
修饰,而且不管测试类中存在多个测试方法,它们在测试方法执行前或执行后只执行一次。
public class DemoFixtureTest {
@BeforeClass
public static void beforeClass() {
System.out.println("DemoFixtureTest.beforeClass");
}
@AfterClass
public static void afterClass() {
System.out.println("DemoFixtureTest.afterClass");
}
@Before
public void before() {
System.out.println("DemoFixtureTest.before");
}
@After
public void after() {
System.out.println("DemoFixtureTest.after");
}
@Test
public void test() {
System.out.println("DemoFixtureTest.test");
}
@Test
public void test2() {
System.out.println("DemoFixtureTest.test2");
}
}
测试卡具可以实现下面功能:
- 在测试前准备输入数据,或配置 Mock 对象
- 加载一个特定的数据库来准备数据
- 为某些测试类创建一些初始化状态
三、JUnit 第三方扩展
一些流行的第三方实现运行器使用 @RunWith
注解来指定:
- Spring
- Spring TestContext Framework 参考手册
- SpringJUnit4ClassRunner Javadoc API
- Mockito
- JMockit
JUnit4 已经是老版本的 JUnit 测试框架,在后面的 JUnit5 中新增了很多新特性,而且简化了单元测试的写法,功能更为强大,掌握了 JUnit4 后,迁移和学习 JUnit5 也会很轻松,读者可以去参考官方文档。
参考文献
JUnit4 官方文档:https://junit.org/junit4/
JUnit5 官方文档:https://junit.org/junit5/docs/current/user-guide/
示例代码
码云:https://gitee.com/code_artist/spring
查看更多教程请关注码匠公众号:CodeArtist