几个Demo让你秒懂Junit5

几个Demo让你秒懂Junit5

Junit5是目前最新版本的的junit单测框架,junit团队在2017年9月10日发布第一个版本。junit5是基于java8及以上版本,支持lambda函数以及其他新的特性。Junit5主要由三部分构成:JUnit Platform + JUnit Jupiter + JUnit Vintage。JUnit Platform用来运行test case的基础平台,同时也开放了一些api用于扩展;JUnit Jupiter是junit5的核心,所有test case的编写都是基于该模块,也支持自定义扩展;JUnit Vintage是用于兼容老版本junit的test case的,可以让老版本的case运行在junit5框架上。

下面先简单介绍下Demo中会用到的通用的特性:

一、如何运行Test Case

1、基于IDE的运行:截至到目前只有IntelliJ IDEA和Eclipse 4.7(Oxygen)支持junit5,只需要添加junit5相应的依赖就可以直接运行。

2、在测试类上利用注解@RunWith(JUnitPlatform.class),可以基于junit4框架运行junit5的case,需要添加依赖junit-4.12、junit-platform-runner、junit-jupiter-engine。这种方式可以在大部分IDE上使用,用于在开发调式test case时使用很是方便,本文中所有demo的开发都是基于这种运行模式。

3、基于构建工具(maven或者gradle)的运行模式,这种方式可以批量且有选择性的执行case,适合运用在CI测试中,需要在构建文件中配置下插件和依赖。

4、console launcher:基于控制台的执行方式,需要下载可执行jar包junit-platform-console-standalone-1.0.2.jar,然后再命令行中执行:

java -jar junit-platform-console-standalone-1.0.2.jar <option>,  这种方式在二次开发分布式运行比较方便,这里不做过多介绍。

本文中的构建配置都是基于maven的,后边demo中会提供详细的配置,客官们莫急。

二、常用的注解

1、@Tag:相当于junit4中的Categories,用于分组,可用于类或方法上,每个类或方法可有多个Tag;用maven运行测试时,可以基于Tag选择性的执行。

2、@TestInstance(Lifecycle.PER_CLASS):用来指定某个测试类的case执行的生命周期,默认的生命周期模式为per_method,即每次执行测试方法都会创建一个单独的测试类实例。per_classs模式是只创建一个测试类实例。还可以通过properties或者pom.xml来配置,demo中都有列出。

3、@ExtendWith:可用于测试类或方法上,用来加载自定义的扩展类,给测试类或方法添加额外的功能,比如输出执行时间

4、@DisplayName:为case定义一个输出时的名称

5、@Disabled:被注解的类或者方法不会被执行,string参数可省略,相当于junit4中的@Ignore,一般用于某些废弃的case上,还可以保留版本信息

三、Little Tips

1、所有的用例类或者方法都无需用public修饰

2、所有被@Test, @TestTemplate, @RepeatedTest, @BeforeAll, @AfterAll, @BeforeEach, or @AfterEach注解的方法,不能有返回值即必须用void修饰

3、unit5中的断言可能不是特别全面,可以依靠第三方库的断言功能来弥补,官方推荐的第三方库: AssertJ, Hamcrest, Truth
直接导入对应的断言方法使用即可。

下面开始给出一些case的demo:

一、标准版case

package com.vdcoding;

/*
 * 被测试类
 */
public class Calculator {
	public int add(int a, int b) {
		return a + b;
	}
	
	public int sub(int a, int b){
		return a-b;
	}
	
}
package com.vdcoding;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestReporter;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

@Tag("standard")
@RunWith(JUnitPlatform.class)
@TestInstance(Lifecycle.PER_CLASS)
public class CalculatorStandardTest {
	/*
	 * 必须为静态方法,在开始执行用例前只初始化一次,相当于junit4中的Before
	 */
	@BeforeAll
	static void beforeAll(){
		System.out.println("Ready to start test!");
		//to do
		
	}
	
	/*
	 * 每开始执行一条用例前都会初始化一次
	 */
	@BeforeEach
	void beforeEach(TestInfo testInfo){
		//to do
		System.out.println(testInfo.getDisplayName());
	}
	
	/*
	 * 必须为静态方法,所有用例执行完后只执行一次,相当于junit4的After
	 */
	@AfterAll
	static void afterAll() {
		System.out.println("All tests finished!");
		//to do
	}
	
	/*
	 * 每执行完一条用例都会执行一次
	 */
	@AfterEach
	void afterEach(TestInfo testInfo){
		System.out.println(testInfo.getTags().toString());
		//to do
	}
	/*
	 * TestInfo为junit内置的注入型参数,包括了当前执行的测试方法的信息,如displayName,tags等
	 */
	@Test
	@DisplayName("standard test")
	@ExtendWith(TimingExtension.class)
	void standardTest01(TestInfo testInfo) {
		Calculator calculator = new Calculator();
		int a = new Random().nextInt();
		int b = new Random().nextInt();
		assertEquals(a + b, calculator.add(a, b), "wrong result");
		assertEquals("standard test", testInfo.getDisplayName(), () -> "TestInfo is injected correctly");
	}
	
	/*
	 * TestReporter为junit内置的注入型参数,可以在执行测试方法时添加额外的数据并默认输出到stdout
	 */
	@Test
	void standardTest02(TestReporter testReporter){
		Map<String, String> map = new HashMap<>();
		map.put("hello", "world");
		testReporter.publishEntry(map);
		testReporter.publishEntry("test", "666");
		assertTrue(true);
	}
	
	@Disabled("no need to test")
	@Test
	void disableTest(){
		//to do
	}
	
	/*控制台输出如下:
	 *  Ready to start test!
	        standard test
		十二月 13, 2017 2:36:22 下午 com.vdcoding.TimingExtension afterTestExecution
		信息: Method [standardTest01] took 10 ms.
		[standard]
		standardTest02(TestReporter)
		ReportEntry [timestamp = 2017-12-13T14:36:22.289, hello = 'world']
		ReportEntry [timestamp = 2017-12-13T14:36:22.292, test = '666']
		[standard]
		All tests finished!
	 */

}

二、可重复执行case

package com.vdcoding;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Random;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestInfo;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

/*
 * @RepeatedTest 重复执行指定的次数,每次的重复执行与正常单次执行的用例生命周期是一样的。
 */
@Tag("repeat")
@RunWith(JUnitPlatform.class)
public class CalculatorRepeatTest {
	/*
	 * 每次用例执行前先输出用例名称与当前执行的次数编号
	 */
	@BeforeEach
	void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo){
		System.out.println(
					"Display Name:" +
					testInfo.getDisplayName() +
					";CurrentRepetition:" +
					repetitionInfo.getCurrentRepetition()
				);
	}
	/*
	 * 重复执行10次
	 */
	@RepeatedTest(10)
	void repeatTest01(TestInfo testInfo){
		Calculator calculator = new Calculator();
		int a = new Random().nextInt();
		int b = new Random().nextInt();
		long result = a - b;
		assertEquals(result, calculator.sub(a, b), "1 + 1 should equal 2");
	}
	/*
	 * 重复执行10次,并指定了一个固定的执行名称Repeat test
	 */
	@RepeatedTest(value=10, name="Repeat test")
	void repeatTest02(TestInfo testInfo){
		Calculator calculator = new Calculator();
		int a = new Random().nextInt();
		int b = new Random().nextInt();
		long result = a - b;
		assertEquals(result, calculator.sub(a, b), "1 + 1 should equal 2");
	}
	
	/*
	 * {displayName}、{currentRepetition}、{totalRepetitions}为RepeatedTest内置的占位符,分别代表
	 * 用例展示名称,当前执行次数编号,总共重复执行次数。其中displayName占位符默认值为方法名称,
	 * 可以通过@DisplayName注解覆盖其值
	 * output:
	 * RepeatTest::1/10
	 * RepeatTest::2/10
	 * ...
	 */
	@RepeatedTest(value=10, name="{displayName}::{currentRepetition}/{totalRepetitions}")
	@DisplayName("RepeatTest")
	void repeatTest03(TestInfo testInfo){
		Calculator calculator = new Calculator();
		int a = new Random().nextInt();
		int b = new Random().nextInt();
		long result = a+b;
		assertEquals(result, calculator.add(a, b), "1 + 1 should equal 2");
	}
	/*
	 * output name:
	 * RepeatTest :: repetition 1 of 10
	 * RepeatTest :: repetition 2 of 10
	 * ...
	 */
	@RepeatedTest(value=10, name=RepeatedTest.LONG_DISPLAY_NAME)
	@DisplayName("RepeatTest")
	void repeatTest04(TestInfo testInfo){
		Calculator calculator = new Calculator();
		int a = new Random().nextInt();
		int b = new Random().nextInt();
		long result = a+b;
		assertEquals(result, calculator.add(a, b), "1 + 1 should equal 2");
	}
}

三、参数化case

package com.vdcoding;

import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.Collection;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

/*
 * 首先,要使用ParameterizedTest注解需要配置junit-jupiter-params依赖;
 * @ParameterizedTest注解必须结合数据源注解使用,可以给同一个测试方法传入不同的参数,每次调用的生命周期与常规的测试方法一样。
 * 示例中演示了比较常用的@ValueSource和@MethodSource,
 * 其他的数据源注解有ArgumentsSource、CsvFileSource、CsvSource、EnumSource,用法请参考官方文档
 */
@RunWith(JUnitPlatform.class)
public class ParameterTest {
	
	/*
	 * @ParameterizedTest的name参数支持内置占位符:
	 * {index}为当前传入的参数值在数据源中的索引值,从1开始;
	 * {0}...{n}为当前传入的参数值,如果传入两个参数,则可以通过{0}{1}来获取参数值
	 * @ValueSource注解只支持简单的原生类型,如 int,string,long,double,参数名加个s,参数值可以为单个值或者数组.
	 */
	@DisplayName("paramTest")
	@ParameterizedTest(name="{index}-current param:{0}")
	@ValueSource(strings={"apple", "banana"})
	void paramTest(String param){
		assertTrue(param instanceof String);
	}
	
	/*
	 * @MethodSource注解引用的方法必须返回Stream、iterator、iterable或者数组。
	 * 被引用的方法必须为测试类中的static方法,如果测试类被@TestInstance(Lifecycle.PER_CLASS)修饰则没有该限制
	 */
	@ParameterizedTest
	@MethodSource("makeString")
	void methodSourceTest(String s){
		assertTrue(s instanceof String);
	}
	
	static Collection<String> makeString(){
		return Arrays.asList("pear", "watermelon");
	}
}

四、Mock测试case

package com.vdcoding;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;
import org.mockito.Mock;

/*
 * 加载自定义的mock扩展类,调用被@Mock注解的类的getName时会返回设置的名称test
 * 自定义的扩展类MockitoExtension中需要定义参数解析器,否则传入测试方法的person参数无法被解析注入。
 */
@RunWith(JUnitPlatform.class)
@ExtendWith(MockitoExtension.class)
public class PersonMockTest {
	@BeforeEach
	void init(@Mock Person person){
		when(person.getName()).thenReturn("test");
		when(person.getAge()).thenReturn(30);
	}
	
	@Test
	void mockTest01(@Mock Person person){
		assertEquals("test", person.getName(), "Person name should be test");
	}
	
	@Test
	void mockTest02(@Mock Person person){
		assertEquals(30, person.getAge());
	}
}
package com.vdcoding;

import static org.mockito.Mockito.mock;
import java.lang.reflect.Parameter;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

/*
 * 自定义的扩展类MockitoExtension
 */
public class MockitoExtension implements TestInstancePostProcessor, ParameterResolver {

	@Override
	public void postProcessTestInstance(Object testInstance, ExtensionContext context) {
		MockitoAnnotations.initMocks(testInstance);
	}

	@Override
	public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
		return parameterContext.getParameter().isAnnotationPresent(Mock.class);
	}

	@Override
	public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
		return getMock(parameterContext.getParameter(), extensionContext);
	}

	private Object getMock(Parameter parameter, ExtensionContext extensionContext) {
		Class<?> mockType = parameter.getType();
		Store mocks = extensionContext.getStore(Namespace.create(MockitoExtension.class, mockType));
		String mockName = getMockName(parameter);

		if (mockName != null) {
			return mocks.getOrComputeIfAbsent(mockName, key -> mock(mockType, mockName));
		}
		else {
			return mocks.getOrComputeIfAbsent(mockType.getCanonicalName(), key -> mock(mockType));
		}
	}

	private String getMockName(Parameter parameter) {
		String explicitMockName = parameter.getAnnotation(Mock.class).name().trim();
		if (!explicitMockName.isEmpty()) {
			return explicitMockName;
		}
		else if (parameter.isNamePresent()) {
			return parameter.getName();
		}
		return null;
	}

}
/*
 * 被测试Person类
 */
public class Person {
	private String name;
	private int age;
	
	public String getName(){
		return name;
	}
	public void setName(String name){
		this.name = name;
	}
	
	public int getAge(){
		return age;
	}
	public void setAge(int age){
		this.age = age;
	}
		
}

五、实现接口的case

package com.vdcoding;

import static org.junit.Assert.assertTrue;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.platform.runner.JUnitPlatform;
import org.junit.runner.RunWith;

/*
 * 通过实现接口的方式,继承一些公共的初始化和清理方法,可以大大减少测试case中的样板代码
 */
@Tag("simpletest")
@RunWith(JUnitPlatform.class)
public class InterfaceImplTest implements TestLifecycleLogger, TimeExecutionLogger {
	
	@Test
	void simpleTest(TestInfo testInfo){
		System.out.println(testInfo.getTags());
		assertTrue(true);
	}
	
	/*output:
	 * 十二月 14, 2017 10:52:19 上午 com.vdcoding.TestLifecycleLogger beforeAllTests
		信息: Before all tests
		十二月 14, 2017 10:52:19 上午 com.vdcoding.TestLifecycleLogger beforeEachTest
		信息: About to execute [simpleTest(TestInfo)]
		[timed, simpletest]
		十二月 14, 2017 10:52:19 上午 com.vdcoding.TimingExtension afterTestExecution
		信息: Method [simpleTest] took 5 ms.
		十二月 14, 2017 10:52:19 上午 com.vdcoding.TestLifecycleLogger afterEachTest
		信息: Finished executing [simpleTest(TestInfo)]
		十二月 14, 2017 10:52:19 上午 com.vdcoding.TestLifecycleLogger afterAllTests
		信息: After all tests
	 */
}
package com.vdcoding;

import java.util.logging.Logger;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExtendWith;

public interface UtilsInterface {}

/*
 * 1、junit5 允许将@Test,@RepeatedTest,@ParameterizedTest,@TestFactory,@TestTemplate, @BeforeEach, @AfterEach
 * 使用在接口的default方法上
 *2、 如果接口类或测试类被@TestInstance(Lifecycle.PER_CLASS)注解,则@BeforeAll和@AfterAll注解可以直接使用在
 * 其default方法上,没有必须使用在static方法上的限制了
 */
@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

    static final Logger LOG = Logger.getLogger(TestLifecycleLogger.class.getName());

    @BeforeAll
    default void beforeAllTests() {
        LOG.info("Before all tests");
    }

    @AfterAll
    default void afterAllTests() {
        LOG.info("After all tests");
    }

    @BeforeEach
    default void beforeEachTest(TestInfo testInfo) {
        LOG.info(() -> String.format("About to execute [%s]", testInfo.getDisplayName()));
    }

    @AfterEach
    default void afterEachTest(TestInfo testInfo) {
        LOG.info(() -> String.format("Finished executing [%s]", testInfo.getDisplayName()));
    }

}

@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}

以上是junit5中比较常用case类型,还有Test Template和Dynamic Tests比较高级和抽象的case类型这里不做介绍,还有开发扩展类的api使用方法及其他详情请参考Junit5官方文档

完整的代码demo,可以从Github上下载!

码字辛苦,转载请注明出处!

 

Comments are closed.