如何使用工具提高Java代码的质量(基于Eclipse)-2

本文详细介绍 JUnit5 的核心概念、配置方法及高级特性,包括断言、条件执行、参数化测试等内容。

软件测试是保证软件质量, 提高软件可靠性的关键, 国外的软件企业都非常注重测试, 江湖上一个流传非常广的一个传说是"微软公司的测试人员与开发人员比例一般为1:1, 甚至在Windows 2000开发团队中, 有1800个测试人员, 900个开发人员, 测试人员与开发人员比例为2:1…"

软件测试是个很大的话题, 上文提到的PMD进行代码审查(Code Review), 是一种静态测试,测试不需要执行代码; 而对应的另一种动态测试, 测试需要实际运行程序, 动态测试又分白盒, 黑盒。

  • 黑盒测试通常也称功能测试, 黑盒测试不关心代码如何实现, 只是从用户的使用角度进行测试;
  • 白盒测试也称结构测试或逻辑驱动测试,是针对被测单元内部是如何进行工作的测试,是针对应用程序的源代码的测试;

本文要分享的JUnit, 是一种单元测试的平台/工具, 属于白盒测试范畴。

在现代软件开发中, 不会等到软件开发完成后才进行功能测试(黑盒测试), 通常在软件的模块编码(构造)阶段就会开始引入单元测试, 测试几乎在软件生命周期的每个阶段。为什么如此早的就引入测试呢? 因为需要采用编码和测试彼此交织, 同步推进的软件方法论, 可以尽早发现bug, 在开发过程开始阶段就识别出主要的风险, 在软件交给功能测试人员时,在功能上基本没有缺陷,可以把尽力放在业务逻辑(功能)测试上,想想如果程序和功能动不动就崩溃/抛异常,程序都还无法稳定运行,是多么奔溃的事。

单元测试可以理解为学习一课程,学习完一个单元后, 进行"单元测试","单元测试"可以评估我们整个单元的学习情况,及时查缺补漏,这样"期末考试"才会更容易取得好成绩。敏捷开发思想就主张持续集成和单元测试, 单元测试可以由开发人员负责编写, 也可以专职测试人员编写, 并无定式, 但确定的是, 测试是有成本, 所以我们需要高效的测试工具或者框架, 提高效率,降低这部分成本。那么就有请本文主角–JUnit。

JUinit简介

JUnit是由Erich Gamma(大牛, GoF之一)和Kent Beck编写的一个开源的单元测试框架。可以帮助开发人员简化测试案例的编写, JUnit是Java社区中知名度非常高的单元测试工具, Eclipse将JUnit作为默认的IDE集成组件。

目前Junit最新版本是JUnit 5, 对Java运行环境的最低要求是Java 8, 与之前的版本不同,JUnit 5由3个不同的子项目的多个不同的模块组成:
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform: 主要功能就是作为测试框架的基础平台, 主要提供测试案例的启动和执行支持(engine, runner, lancher, …)
JUnit Jupiter : Jupiter(木星)是JUnit 5的代号, 这个包下的模块包含JUnit 5的主要功能;
JUnit Vintage : 这个包下的模块可以让我们在新的JUnit平台上运行旧版本(JUnit 3 /4)的测试;

从JUnit 4开始, 利用了Java 5.0的新特性注解 (Annotation)来标注测试, 这使得测试案例的编写更为简单灵活。我们只需要使用JUnit提供的注解给普通的类和方法进行标注,就能使用到JUnit提供的测试功能。

本文是基于JUnit5的,JUnit5的新功能在低版本中是无法使用的,JUnit3/4的测试代码, 也不是所有都能拿到JUnit5中执行的, 也需要做一些额外的配置/迁移。

在开始之前说一些题外话, 单元测试不一定非要什么测试框架, 就写个main函数, 在里面编写一些测试代码行不行? 当然可以, 使用工具是方法, 测试是目的, 但在复杂项目测试时, 使用这种方法有很多局限, 什么都要自己实现工作量非常大, 效率非常低, 这也是引入自动化测试工具/平台的动因。

无论使用什么方法, 必须将单元测试代码和最终的交付的代码分开, 测试代码要物理上分开不同文件夹存放, 不要直接在最终代码上写测试。首先你很可能忘记删除测试代码, 把测试代码带到生产中, 造成软件风险和缺陷。其次就是测试案例不能重用, 你辛辛苦苦写好的测试代码, 测试完了交付前清除掉了。而后续版本迭代/修复Bug修复, 又要重写测试代码, 然后又清除, 又编写, … 怎么这么多又, 内心万马奔腾。

JUnit 基于Eclipse配置

JUnit5的一些安装需求:

  • JUnit 5运行需要Java8或者8以上的版本;
  • Eclipse内建支持JUnit, 从Oxygen.1a (4.7.1a)版本开始支持了JUnit 5;
  • Apache Maven项目需要使用maven-surefire-plugin插件;

Eclipse普通的java项目

Eclipse默认创建的Java的项目, 只有一个源代码目录src, 这不利于我们将测试代码分开存放, 我们可以按照Maven的目录结构进行创建, 创建新的两个源代码文件夹:
src/main/java -> 用于存放程序代码;
src/test/java -> 用于存放测试代码;
并将默认的源代码目录src删除
在这里插入图片描述
Eclipse内嵌了JUnit的库,你可以在创建项目时将它添加到项目(注意, 这里要选中Classpath),也可以在后续添加。
在这里插入图片描述
创建好后, 把程序代码放在src/main/java目录下, 创建的测试代码选中源目录src/main/test, 编写完测试代码,直接点Eclipse的执行按钮,Run As选中"JUnit Test", 就可以执行单元测试了。
在这里插入图片描述

Eclipse的Maven项目

如果我们在Eclipse中创建maven项目时, 选的Archetype为"maven-archetype-quickstart",默认使用的是JUnit3, 且默认使用maven-compiler-plugin插件指定的source/target的版本都是1.6,不满足JUnit5最低1.8的要求, 也需要额外配置。

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

Apache Maven针对JUnit的配置, 官方文档有详细说明, 可以参考, 下面是我自己整理的Eclipse+Maven+JUnit5的pom.xml配置:

	<dependencies>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter</artifactId>
			<version>5.7.2</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.junit.platform</groupId>
			<artifactId>junit-platform-runner</artifactId>
			<version>1.7.2</version>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>1.8</source>
					<target>1.8</target>
				</configuration>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>2.22.2</version>
			</plugin>
		</plugins>
	</build>

配置完成后, 可以直接点执行按钮进行测试, 也可以通等mvn相关命令如(Maven Test)执行,你编写的测试案例。

测试类和测试方法(Test Classes and Methods)

在JUnit5中:
测试类(Test Class): 任何一个包含至少一个测试方法的普通java类, 叫测试类;
测试方法(Test Method): 任何被以下注解(Annotation)直接标注的普通java实例方法:
@Test
@RepeatedTest
@ParameterizedTest
@TestFactory
@TestTemplate

生命周期方法(Lifecycle Method): 任何被以下注解直接标注的普通方法:
@BeforeAll,
@AfterAll,
@BeforeEach,
@AfterEach,

注意:
(1). 测试类不能是抽象的, 只能有一个构造函数;
(2). 测试方法不能是抽象的, 且不能有返回值;
(3). 测试类, 测试方法, 生命周期方法可以不是public的,但是不能为private的;
(4). 测试方法, 生命周期方法, 构造方法在JUnit5之前是不允许又参数的, JUnit5版本允许有参数, 但参数类型有要求的(参考后面的"Dependency Injection for Constructors and Methods");

编写测试

显示名称(Display Names)

JUnit默认显示的是测试类和测试方法可以通过@DisplayName自定义显示名, 显示名可以包含空格,特殊字符等。

断言(Assertions)

测试中核心之一, 断言是一系列的方法, 通过给定的条件程序化的判断一个测试的结果是否通过(success or fail)。(Assertions is a collection of utility methods that support asserting conditions in tests. ).

JUnit 5(Jupiter)引入了大量新的断言方法, 并且可以使用Java 8的Lambda表达式,并支持Kotlin断言和第三方断言库(如 AssertJ, Hamcrest, Truth等)。所有的Junit5自带的断言都是静态方法,定义在org.junit.jupiter.api.Assertions类中.

断言函数如果判断条件失败(如), 就会抛出AssertionFailedError异常, Junit对抛出此异常的方法判定为失败;

对于一个测试方法, 只要代码中抛出任何异常,都会被判定为测试不通过。如果要测试异常, 可以用assertThrows()/assertDoesNotThrow()断言。

假定(Assumptions)

Assumptions.class in JUnit5 contains a collection of methods that enables us to programmatically decide whether to continue executing the test or skip the further execution.

Assumptions(org.junit.jupiter.api.Assumptions)是一组方法, 通过该方法可以让我们通过编程的方式决定是继续执行测试还是跳过测试。

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

@DisplayName("JUnit测试案例1")
public class Testcase1 {
	
	@DisplayName("Assertions测试1")
	@Test
	void testAssertions1() {
		assertEquals(2, 1 + 1);
	}
	
	@DisplayName("Assertions测试2")
	@Test
	void testAssertions2() {
		//如果测试方法有抛出异常, 也会被判定为失败
		int i = Integer.parseInt("a");
		assertEquals(2, 1 + 1);
	}
	
	@DisplayName("Assertions测试3")
	@Test
	void testAssertions3() {
		// 
		Throwable exception = assertThrows(NumberFormatException.class, () -> {
			Integer.parseInt("a");
		});
	}

	@DisplayName("Assumptions测试1")
	@Test
	void testAssumption1() {
		String osName = System.getProperty("os.name").toLowerCase();
		boolean isWindows = (osName.indexOf("win") >= 0);
		assumeTrue(isWindows);
		// 上述代码的意图是只有在Windows平台,后续测试才执行, 否者就跳过, 测试结果为Skip.
		// test some base on Windows....
	}

	@DisplayName("Assumptions测试2")
	@ParameterizedTest
	@ValueSource(ints = { 1, -1, 2, 3 })
	void testAssumption2(int i) {
		assumeTrue(i >= 0, "错误的输入,仅接受正整数,跳i小于0的测试。");
		try {
			Thread.sleep(i);
		} catch (InterruptedException e) {
		}
	}
}

测试的结果如下:
在这里插入图片描述

禁止测试(Disabling Tests)

我们可以通过@Disabled注解来禁止一个测试类或者测试方法。

// DisabledClassDemo测试类的所有测试方法都将不会被执行.
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
    @Test
    void testWillBeSkipped() {}
}

class DisabledTestsDemo {
    // 只有testWillBeSkipped测试方法被禁止。
    @Disabled("Disabled until bug #42 has been resolved")
    @Test
    void testWillBeSkipped() {}
    
    @Test
    void testWillBeExecuted() {}
}

执行结果:
在这里插入图片描述

根据条件执行测试(Conditional Test Execution)

The ExecutionCondition extension API in JUnit Jupiter allows developers to either enable or disable a container or test based on certain conditions programmatically.

JUnit5提供了一些列的API, 以帮助开发者根据条件通过编程控制是否执一个测试。JUnit内建列一列的条件, 在org.junit.jupiter.api.condition包下, 主要分为3大类:

Operating System Conditions

根据操作系统类型为条件判断是否执行指定的测试, 主要包括以下注解:

  • @EnabledOnOs/@DisabledOnOs
@Test
@EnabledOnOs(OS.MAC)
void onlyOnMacOs() {}

@Test
@DisabledOnOs(OS.WINDOWS)
void notOnWindows() {}

@Test
@EnabledOnOs({ OS.LINUX, OS.MAC })
void onLinuxOrMac() {}

Java Runtime Environment Conditions

根据JRE的版本作为条件判断是否执行指定的测试, 主要包括以下注解:

  • @EnabledOnJre / @DisabledOnJre
  • @EnabledForJreRange / @DisabledForJreRange
@EnabledOnJre(JRE.JAVA_8)
void onlyOnJava8() {}

@Test
@EnabledOnJre({ JRE.JAVA_9, JRE.JAVA_10 })
void onJava9Or10() {}

@Test
@EnabledForJreRange(min = JRE.JAVA_9, max = JRE.JAVA_11)
void fromJava9to11() {}

@Test
@EnabledForJreRange(min = JRE.JAVA_9)
void fromJava9toCurrentJavaFeatureNumber() {}

@Test
@EnabledForJreRange(max = JRE.JAVA_11)
void fromJava8To11() {}

@Test
@DisabledOnJre(JRE.JAVA_9)
void notOnJava9() {}
@Test
@DisabledForJreRange(min = JRE.JAVA_9, max = JRE.JAVA_11)
void notFromJava9to11() {}

@Test
@DisabledForJreRange(min = JRE.JAVA_9)
void notFromJava9toCurrentJavaFeatureNumber() {}

@Test
@DisabledForJreRange(max = JRE.JAVA_11)
void notFromJava8to11() {}

System Property Conditions

根据系统的系统属性来判断是否执行指定的测试, 可以通过System.getProperties()所有的系统属性, 主要包括以下注解:

  • @EnabledIfSystemProperty / @DisabledIfSystemProperty
@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {}

@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {}

Environment Variable Conditions

根据系统的环境变量来判断是否执行指定的测试, 可以通过System.getenv()获取所有的系统属性, 主要包括以下注解:

  • @EnabledIfEnvironmentVariable / @DisabledIfEnvironmentVariable
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {}

@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {}

Custom Conditions

可以之定义条件, 主要包括以下注解:

  • @EnabledIf / @DisabledIf
@Test
@EnabledIf("customCondition")
void enabled() {}

@Test
@DisabledIf("customCondition")
void disabled() {}

boolean customCondition() {
    return true;
}

标签和过滤(Tagging and Filtering)

可以通过@Tag主机对测试类或者测试方法打标签, 这些tag能用于后续执行时进行过滤, 这有点类似于对测试进行分组。

标签(Tag)命名应该遵循如下规则:

  • 标签不能为null或者为空;
  • 标签中不能包含空白字符(whitespace);
  • 标签中不能包含ISO控制字符;
  • 标签中不能包含保留字符: , ( ) & | !
// 例如,我们按不同特性(feature)为测试方法打上标签"feature-X";
// 在执行测试时指定包含/排除某一(些)标签,这样我们就可以有选择的只执行某类特性的效果;
@Test
@Tag("feature-1")
void test1() {}

@Test
@Tag("feature-2")
void test2() {}

@Tag("feature-1")
@Tag("feature-3")
@Test
void test3() {}

@Test
@Tag("feature-3")
void test4() {}

我们可以在maven执行测试阶段指定过滤:

方法一: 过滤条件写在pom.xml中surefire插件配置groups/excludedGroups。

	<build>
		<plugins>
		...
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-surefire-plugin</artifactId>
				<version>2.22.2</version>
				<configuration>
					<groups>feature-1, feature-2</groups>
					<excludedGroups>feature-3</excludedGroups>
				</configuration>
			</plugin>
		</plugins>
	</build>

方法2: 通过命令行参数自定(-Dgroups/-DexcludedGroups):
$ mvn -Dgroups="feature-1, feature-2" -DexcludedGroups="feature-1" test

这时只会执行包含feature-1, feature-2标签,但不包含feature-3标签的测试类和测试方法:
...
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.littlestar.junit1.Testcase3
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.023 s - in org.littlestar.junit1.Testcase3
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.658 s
[INFO] Finished at: 2021-08-03T11:00:17+08:00
[INFO] ------------------------------------------------------------------------
...

输出可以看到只执行了2个测试,test1,test2,test3测试方法包含feature-1, feature-2标签, 但测试方法test3还包含了feature-3标签, 被排除了,所以最终只执行了test1, test2两个测试方法。

我们还可以在 套件测试(Suit Test)中使用标签过滤, JUnit可以把测试组合在一起执行,称为Suit Test.

import org.junit.platform.runner.JUnitPlatform;
import org.junit.platform.suite.api.ExcludeTags;
import org.junit.platform.suite.api.IncludeTags;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.runner.RunWith;

@RunWith(JUnitPlatform.class)
@SelectPackages("org.littlestar.junit1")
@SelectClasses(Testcase3.class)
@IncludeTags({"feature-1","feature-2"})
@ExcludeTags({"feature-3"})
public class Testcase4 {
}

执行结果

测试方法执行顺序(Test Execution Order)

JUnit根据确定的排序算法确定测试方法的执行顺序, 默认的方法排序算法 (MethodSorters.DEFAULT)是顺序不可知(unpredictable order), 也就是默认情况下测试方法的执行顺序是不可知的。

JUnit5中允许我们通过@TestMethodOrder指定测试方法排序的算法,以控制测试方法的执行顺序。有以下的测试内置的方法排序算法,这些算法定义在org.junit.jupiter.api.MethodOrderer接口内:

  • DisplayName: 基于测试方法的DisplayName的字符顺序;
  • MethodName: 基于测试方法的方法名的字符顺序;
  • OrderAnnotation: 基于指定的@Order注解;
  • Random: 基于随机排序;
//@TestMethodOrder(MethodOrderer.DisplayName.class)
//@TestMethodOrder(MethodOrderer.MethodName.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
//@TestMethodOrder(MethodOrderer.Random.class)
//@TestMethodOrder(MethodOrderer.Alphanumeric.class) //已经废弃(Deprecated in favor of MethodName. Will be removed in 6.0)
public class Testcase5 {
	private static StringBuilder output = new StringBuilder("");

	@Test
	@Order(1)
	void testOrder1() {
		output.append("1");
	}

	@Test
	@Order(2)
	void testOrder2() {
		output.append("2");
	}

	@Test
	@Order(3)
	void testOrder3() {
		output.append("3");
	}

	@AfterAll
	public static void orderOutput() {
		System.out.println(output.toString());
	}
}

测试(类)实例生命周期(Test Instance Lifecycle)

为了避免测试方法对实例状态的改变而影响其他测试方法返回正确的结果,对于每一个测试方法,都会重新创建一个类实例,也可以所有的测试方法都使用同一个实例,则在测试类使用@TestInstance注解进行设置:

  • @TestInstance(TestInstance.Lifecycle.PER_METHOD)每个测试方法都创建一个实例; (默认)
  • @TestInstance(TestInstance.Lifecycle.PER_CLASS)所有的测试方法都使用同一个实例;

注意:在PER_CLASS模式下, beforeAll/afterAll生命周期方法可以不是静态的(static)。

@DisplayName("测试(类)实例生命周期")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class Testcase7 {
	public Testcase7() { System.out.println("Constructor()"); }
	@BeforeAll 
	void beforeAll() { System.out.println("BeforeAll()");}
	
	@BeforeEach
	void beforeEach() { System.out.println("BeforeEach()");}
	
	@Test
	void test1() {System.out.println("test1()");}
	
	@Test
	void test2() {System.out.println("test2()");}
	
	@AfterEach
	void afterEach() { System.out.println("AfterEach()");}
	
	@AfterAll
	void afterAll() { System.out.println("AfterAll()"); }
}
/**
测试输出:
Constructor() --> 执行构造函数, 创建测试类实例;
BeforeAll() --> 非静态的BeforeAll在构造方法之后执行;
BeforeEach()
test1()
AfterEach()
BeforeEach()
test2()
AfterEach()
AfterAll()
*/

@DisplayName("测试(类)实例生命周期")
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
public class Testcase7 {
	public Testcase7() { System.out.println("Constructor()"); }
	@BeforeAll
	static void beforeAll() { System.out.println("BeforeAll()");}
	// ...
	@AfterAll
	static void afterAll() { System.out.println("AfterAll()"); }
}
/**
测试输出:
BeforeAll()
Constructor() --> 每个测试方法一个测试类实例
BeforeEach()
test1()
AfterEach()
Constructor() --> 每个测试方法一个测试类实例
BeforeEach()
test2()
AfterEach()
AfterAll()
*/

嵌套测试(Nested Tests)

一个Java类中可以嵌套一个或者多个内部类(Inner/Nest Class), 类似的一个JUnit测试类内也可以嵌套测试类, 在测试类的内部类添加@Nested注解, 那么这个这个内部类就是一个嵌套测试类。

注意, 因为内部类不能包含static方法,所以嵌套测试类不能定义@BeforeAll/@AfterAll生命周期方法。

@DisplayName("嵌套测试类测试用例")
public class Testcase6 {
	//public Testcase6() { System.out.println("..Constructor Outer");}
	
	@BeforeAll
	static void outerBeforeAll() { System.out.println("BeforeAll Outer");}
	
	@BeforeEach
	void outerBeforeEach() { System.out.println("..BeforeEach Outer");}
	
	@Test
	void testOuter1() { System.out.println("....Test Outer"); }
	
	@Test
	void testOuter2() { System.out.println("....Test Outer"); }
	
	@Nested
	@DisplayName("嵌套用例1")
	class NestedClass1 {
		@BeforeEach
		void nested1BeforeEach() { System.out.println("....BeforeEach Nested1");}
		//public NestedClass1() { System.out.println("....Constructor Nested1");}
		
		@Test
		void testNested1() {System.out.println("......Test Nested1");}
		
		@AfterEach
		void nested1AfterEach() { System.out.println("....AfterEach Nested1");}
	}

	@Nested
	@DisplayName("嵌套用例2")
	class NestedClass2 {
		@BeforeEach
		void nested2BeforeEach() { System.out.println("....BeforeEach Nested2");}
		//public NestedClass2() { System.out.println("....Constructor Nested2");}
		
		@Test
		void testNested2() {System.out.println("......Test Nested2");}
		
		@AfterEach
		void nested2AfterEach() { System.out.println("....AfterEach Nested2");}
	}
	
	@AfterEach
	void outerAfterEach() { System.out.println("..AfterEach Outer"); }
	
	@AfterAll
	static void outerAfterAll() { System.out.println("AfterAll Outer"); }
}

/**
测试输出如下:
BeforeAll Outer
..BeforeEach Outer
....Test Outer
..AfterEach Outer
..BeforeEach Outer
....Test Outer
..AfterEach Outer
..BeforeEach Outer
....BeforeEach Nested1
......Test Nested1
....AfterEach Nested1
..AfterEach Outer
..BeforeEach Outer
....BeforeEach Nested2
......Test Nested2
....AfterEach Nested2
..AfterEach Outer
AfterAll Outer
*/

测试类构造和测试方法依赖注入(Dependency Injection for Constructors and Methods)

在JUnit5之前的版本, 测试类的构造函数和测试方法不允许包含参数。而JUnit5可以通过ParameterResolver(接口)定义的API来在运行时过程中动态解析参数。

JUnit5有3种内建的ParameterResolver接口实现:

  • TestInfoParameterResolver: 构造方法和测试方法允许使用TestInfo参数类型;
  • RepetitionInfoParameterResolver: 生命周期方法(@RepeatedTest, @BeforeEach, or @AfterEach)允许使用RepetitionInfo参数类型;
  • TestReporterParameterResolver: 构造方法和测试方法允许使用TestReporter参数类型;
@Tag("Tag1")
@Tag("Tag2")
public class Testcase8 {
	public Testcase8(TestInfo testInfo) {
		System.out.println("TestInfo.getDisplayName(): " + testInfo.getDisplayName());
		System.out.println("TestInfo.getTags(): " + Arrays.toString(testInfo.getTags().toArray()));
	}
	
	@BeforeEach
	void beforeEach(RepetitionInfo repetitionInfo){
		//...
	}
	
	@Test
	void test1(TestReporter testReporter) {
		testReporter.publishEntry("a status message");
	}
}

重复测试(Repeated Tests)

JUnit Jupiter提供@RepeatedTest注解, 可以重复执行指定次数的测试方法。

public class Testcase9 {
	@BeforeEach
	void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
		int currentRepetition = repetitionInfo.getCurrentRepetition();
		int totalRepetitions = repetitionInfo.getTotalRepetitions();
		String methodName = testInfo.getTestMethod().get().getName();
		System.out.println(String.format("About to execute repetition %d of %d for %s", //
				currentRepetition, totalRepetitions, methodName));
	}

	@DisplayName("重复测试5次")
	@RepeatedTest(5)
	void repeatedTest() {}

	// 可以设置每次执行的显示的名称
	@RepeatedTest(value = 2, name = "{displayName} {currentRepetition}/{totalRepetitions}")
	@DisplayName("重复测试: ")
	void customDisplayName(TestInfo testInfo) {}
}

参数化测试(Parameterized Tests)

参数化测试可以让我们多次使用不通的参数,重复多次执行测试方法。参数化测试的注解为@ParameterizedTest。

设置要求

要执行参数化测试,Maven的话需要junit-jupiter-params依赖, 如果是Eclipse非Maven项目, 只要导入JUnit5的库, 就已经包含了相应的jar包。

参数源(Sources of Arguments)

参数源就是要参数的来源, JUnit Jupiter非常丰富的参数源注解:

@ValueSource

@ValueSource注解可以让你指定一个数组, 执行参数化测试时,将会遍历每个组成员,将每个成员作为参数传递个测试方法。@ValueSource支持以下数据类型:

  • short
  • byte
  • int
  • long
  • float
  • double
  • char
  • boolean
  • java.lang.String
  • java.lang.Class
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
	System.out.println(argument);
}
Null and Empty Sources

通常我们测试我们的软件是否能处理一些有问题的输入值(bad input), 我们需要在参数里送null或空值, 这些特殊参数时不能直接写到成员列表中的, 如是不允许@ValueSource(strings = { null, …})的,这时就需要一些特殊注解来定义空值。

  • @NullSource: 为@ParameterizedTest标记的参数测试方法传递一个空值;
  • @EmptySource: 为@ParameterizedTest标记的参数测试方法传递一个空源;
  • @NullAndEmptySource: @NullSource 和 @EmptySource的组合;
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { "Abc", "Def" })
void nullEmptyAndBlankStrings(String text) {
	System.out.println("Input: '" + text+"'");
}
/**
测试输出:
Input: 'null'
Input: ''
Input: 'Abc'
Input: 'Def'
*/
@EnumSource

@EnumSource允许为参数化测试传递枚举类型参数。

import org.junit.jupiter.pa允许为参数化测试传递枚举类型参数。rams.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;

public class Testcase10 {
	
	enum Fruit {
	 Apple, Pear, Strawberry;	
	}
	
	@ParameterizedTest
	@EnumSource(Fruit.class) //传递Fruit所有的枚举字段(Fruit.values());
	void testWithEnumSource(Fruit unit) {
		
	    System.out.println("Input: '" + unit.name()+"'");
	}

	@ParameterizedTest
	@EnumSource(names = { "Apple", "Pear" }) // 仅传递"Apple", "Pear"
	void testWithEnumSourceInclude(Fruit unit) {
		System.out.println("Input: '" + unit.name()+"'");
	}
	
	@ParameterizedTest
	//Fruit所有枚举字段, 但除了"Apple", "Pear"
	@EnumSource(mode = EnumSource.Mode.EXCLUDE, names = { "Apple", "Pear" })
	void testWithEnumSourceExclude(Fruit unit) {
		System.out.println("Input: '" + unit.name()+"'");
	}
	
	@ParameterizedTest
	//Fruit所有枚举A开头的字段.
	@EnumSource(mode = EnumSource.Mode.MATCH_ALL, names = "^A.*")
	void testWithEnumSourceRegex(Fruit unit) {
		System.out.println("Input: '" + unit.name()+"'");
	}
}
@MethodSource

@MethodSource 允许我们通过编写工厂方法的方式提供参数源。
注意:

  1. 工厂方法必须为静态(static), 除非指定测试类实例生命周期为@TestInstance(Lifecycle.PER_CLASS);
  2. 工厂方法不能有参数;
  3. 工厂方法必须返回Stream类型参数, 作为参数源;
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
	System.out.println("Input: '" + argument + "'");
}

static Stream<String> stringProvider() {
	List<String> collections = Arrays.asList("Apple", "Pear", "Strawberry", "Banana");
	Stream<String> stream = collections.parallelStream();
	return stream;
}
@CsvSource

@CsvSource 允许我们使用CSV风格定义(逗号分隔)参数源。

@ParameterizedTest
@CsvSource(value = {
    "apple        , 1",
    "banana,        2",
    "'lemon, lime', 0xF1",
    ", 3",
    "NIL, 4"
}, nullValues = "NIL")
void testWithCsvSource(String fruit, int rank) {
	System.out.println("Fruit: '" + fruit + "', Rank: '"+rank+"'");
}
/**
测试输出:
Fruit: 'apple', Rank: '1'
Fruit: 'banana', Rank: '2'
Fruit: 'lemon, lime', Rank: '241'
Fruit: 'null', Rank: '3'
Fruit: 'null', Rank: '4'
*/
@CsvFileSource

@CsvFileSourcelets you use CSV files from the classpath or the local file system.

@ParameterizedTest
// Eclipse项目的ClassPath的配置可以参考项目根目录下的.classpath文件
@CsvFileSource(resources = "/org/littlestar/junit1/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromClasspath(String country, int reference) {
	System.out.println("Country: '" + country + "', Reference: '"+reference+"'");
}

@ParameterizedTest
@CsvFileSource(files = "C:\\Users\\Think\\eclipse-workspace\\junit1\\target\\classes\\org\\littlestar\\junit1\\two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
	System.out.println("Country: '" + country + "', Reference: '"+reference+"'");
}
@ArgumentsSource

可以通过编写ArgumentsProvider接口实现类来提供参数源。
要求:

  • 自定义类必须实现ArgumentsProvider接口;
  • 如果定义为嵌套类(nested class),必须是静态(static)的;
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
	System.out.println("Fruit: '" + argument + "'");
}

static class MyArgumentsProvider implements ArgumentsProvider {
	@Override
	public Stream<? extends Arguments> provideArguments(ExtensionContext context) throws Exception {
		return Stream.of("Apple", "Pear", "Strawberry", "Banana").map(Arguments::of);
	}
}

超时(Timeouts)

@Timeout主机允许我们指定一个测试方法的最大执行实践,如果超过了这个时间,测试将判断为失败。默认事件单位为秒,可以通过参数指定其它事件单位。

@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds100Milliseconds() {
	try {
		TimeUnit.MILLISECONDS.sleep(101);
	} catch (InterruptedException e) {
	}
	assertTrue(true);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值