写点什么

原创 | TDD 工具集:JUnit、AssertJ 和 Mockito (十九) 编写测试 - 依赖注入\测试接口\重复测试

发布于: 2020 年 06 月 06 日
原创 | TDD工具集:JUnit、AssertJ和Mockito (十九)编写测试-依赖注入\测试接口\重复测试







本文分享在编写测试中“依赖注入、测试接口、重复测试”三节内容的方法。

依赖注入

重要性:★★☆☆☆

JUnit Jupiter允许测试类的构造函数、测试方法和生命周期方法接受参数。这些参数在运行时通过预先注册的参数解析器ParameterResolver的实例进行解析。

1. 内建的参数解析器

有3个内建的参数解析器,在JUnit中它们是自动注册的:

  • TestInfoParameterResolver

如果测试类的构造函数、测试方法或生命周期方法的参数类型是TestInfo,内建的参数解析器TestInfoParameterResolver将提供一个TestInfo的实例作为这些方法的参数值。可以使用这个TestInfo实例来检索当前测试容器或测试方法的相关信息,例如显示名称、测试类、测试方法以及相关的标签等。

下面的代码显示如何获取注入构造函数、生命周期函数和测试方法的TestInfo的内容:

package yang.yu.tdd.di;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
@DisplayName("TestInfo Demo")
class TestInfoDemo {
TestInfoDemo(TestInfo testInfo) {
assertThat(testInfo.getDisplayName()).isEqualTo("TestInfo Demo");
}
@BeforeEach
void init(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertThat(displayName).isIn("TEST 1", "test2()");
}
@Test
@DisplayName("TEST 1")
@Tag("my-tag")
void test1(TestInfo testInfo) {
assertThat(testInfo.getDisplayName()).isEqualTo("TEST 1");
assertThat(testInfo.getTags()).contains("my-tag");
}
@Test
void test2() {
}
}
  • RepetitionInfoParameterResolver

如果测试类的构造函数、测试方法或生命周期方法拥有注解@RepeatedTest@BeforeEach@AfterEach,并且接受类型为RepetitionInfo的参数,参数解析器RepetitionInfoParameterResolver将提供一个RepetitionInfo实例作为这些方法的参数值。可以从RepetitionInfo中获取当前重复次数以及总重复次数等相关信息。

下面是示例代码:

package yang.yu.tdd.di;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
public class RepetitionInfoDemo {
@RepeatedTest(5)
void repeat(RepetitionInfo repetitionInfo) {
System.out.println("Current Repetition: " + repetitionInfo.getCurrentRepetition());
System.out.println("Total Repetitions: " + repetitionInfo.getTotalRepetitions());
}
}
  • TestReporterParameterResolver

如果测试类的构造函数、测试方法或生命周期方法的参数类型是TestReporter,参数解析器TestReporterParameterResolver将提供一个TestReporter实例作为这些方法的参数值。可以使用这个TestReporter向当前测试的测试报告添加额外的数据。这些数据可以被TestExecutionListenerreportingEntryPublished()方法消费,使它们可以显示在IDE视图和测试报告中。

下面是示例代码:

package yang.yu.tdd.di;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestReporter;
import java.util.HashMap;
import java.util.Map;
class TestReporterDemo {
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a status message");
}
@Test
void reportKeyValuePair(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
@Test
void reportMultipleKeyValuePairs(TestReporter testReporter) {
Map<String, String> values = new HashMap<>();
values.put("user name", "dk38");
values.put("award year", "1974");
testReporter.publishEntry(values);
}
}

2. 自定义参数解析器

可以通过创建自定义的参数解析器并通过@ExtendWith注解注册到被测试类,来让测试类构造函数、测试方法、生命周期方法注入特定类型的参数。

下面创建一个自定义的参数解析器RandomParametersExtension,这个参数解析器在测试方法中查找拥有@Random注解的int型参数,提供一个随机整数值:

package yang.yu.tdd.di;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
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.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
public class RandomParametersExtension implements ParameterResolver {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Random {
}
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.isAnnotated(Random.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return getRandomValue(parameterContext.getParameter(), extensionContext);
}
private Object getRandomValue(Parameter parameter, ExtensionContext extensionContext) {
Class<?> type = parameter.getType();
java.util.Random random = extensionContext.getRoot().getStore(Namespace.GLOBAL)//
.getOrComputeIfAbsent(java.util.Random.class);
if (int.class.equals(type)) {
return random.nextInt();
}
if (double.class.equals(type)) {
return random.nextDouble();
}
throw new ParameterResolutionException("No random generator implemented for " + type);
}
}

下面的代码显示通过注册RandomParametersExtension参数解析器,给测试方法注入随机整数:

package yang.yu.tdd.di;
import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.assertj.core.api.Assertions.assertThat;
import static yang.yu.tdd.di.RandomParametersExtension.*;
@ExtendWith(RandomParametersExtension.class)
class MyRandomParametersTest {
@Test
void injectsInteger(@Random int i, @Random int j) {
assertThat(i).isNotEqualTo(j);
}
@Test
void injectsDouble(@Random double d) {
assertThat(d).isCloseTo(0.0, Offset.offset(1.0));
}
}



测试接口

重要性:★★★☆☆

从Java 8开始,可以在接口上定义默认方法和静态方法,包含实现代码。

JUnit Jupiter允许在接口的默认方法上声明@Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate@BeforeEach, 和@AfterEach注解,在接口的静态方法上声明 @BeforeAll 和 @AfterAll 注解。如果采用PER_CLASS生命周期的话,也可以在接口的默认方法上声明 @BeforeAll 和 @AfterAll 注解。实现这些接口的测试方法将继承这些方法和注解。

注解@Tag@ExtendWith也可以声明在接口上,由实现这些接口的测试类继承。

下面的代码创建一个TestLifecycleLogger接口,定义了生命周期相关方法。

package yang.yu.tdd.iface;
import org.junit.jupiter.api.*;
import java.util.logging.Logger;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
interface TestLifecycleLogger {
static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());
@BeforeAll
default void beforeAllTests() {
logger.info("Before all tests");
}
@AfterAll
default void afterAllTests() {
logger.info("After all tests");
}
@BeforeEach
default void beforeEachTest(TestInfo testInfo) {
logger.info(() -> String.format("About to execute [%s]",
testInfo.getDisplayName()));
}
@AfterEach
default void afterEachTest(TestInfo testInfo) {
logger.info(() -> String.format("Finished executing [%s]",
testInfo.getDisplayName()));
}
}

下面的代码创建TestInterfaceDynamicTestsDemo接口,定义了注解为@TestFactory的动态测试生成方法dynamicTestsForPalindromes()

package yang.yu.tdd.iface;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
interface TestInterfaceDynamicTestsDemo {
@TestFactory
default Stream<DynamicTest> dynamicTestsForPalindromes() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
static boolean isPalindrome(String raw) {
String str = "";
for (int i = 0; i < raw.length(); i++) {
char ch = raw.charAt(i);
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
str += ch;
}
}
str = str.toLowerCase();
int end = str.length();
for (int i = 0; i < end / 2; i++) {
if (str.charAt(i) != str.charAt(end - i - 1)) {
return false;
}
}
return true;
}
}

下面的代码创建测试类TestInterfaceDemo,它继承了上面的几个接口:

package yang.yu.tdd.iface;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class TestInterfaceDemo implements TestInterfaceDynamicTestsDemo, TestLifecycleLogger {
@Test
void isEqualValue() {
assertThat("a".length()).as("is always equal").isEqualTo(1);
}
}

当测试类TestInterfaceDemo执行时,它所实现的各个接口上定义的测试方法、生命周期方法、测试工厂方法也会执行,就如同这些方法是直接定义在测试类上面一样。



重复测试

重要性:★★☆☆☆

通过声明@RepeatedTest注解取代@Test`注解,JUnit可以让一个测试方法重复执行若干次。

@RepeatedTest(10)
void repeatedTest() {
// ...
}

上面的测试方法repeatedTest()会重复执行10次。

还可以通过RepeatedTest注解的name属性给测试方法定制显示名:

package yang.yu.tdd.repeated;
import java.util.logging.Logger;
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.TestInfo;
import static org.assertj.core.api.Assertions.assertThat;
class RepeatedTestsDemo {
private Logger logger = Logger.getLogger(RepeatedTestsDemo.class.getName());
@BeforeEach
void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();
String methodName = testInfo.getTestMethod().get().getName();
logger.info(String.format("About to execute repetition %d of %d for %s", //
currentRepetition, totalRepetitions, methodName));
}
@RepeatedTest(10)
void repeatedTest() {
// ...
}
@RepeatedTest(5)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
assertThat(repetitionInfo.getTotalRepetitions()).isEqualTo(5);
}
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertThat(testInfo.getDisplayName()).isEqualTo("Repeat! 1/1");
}
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertThat(testInfo.getDisplayName()).isEqualTo("Details... :: repetition 1 of 1");
}
@RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
}
}

方法的最终显示名可以是通过@DisplayName注解定义的静态显示名和通过@RepeatedTest注解的name属性定义的动态显示名的组合。

在通过@RepeatedTest注解的name属性定义的动态显示名,可以获取下面的静态内容:

  • {displayName}:方法上的注解 @RepeatedTest 的值。

  • {currentRepetition}:当前是第几次执行。

  • {totalRepetitions}:总重复次数。



这一节就讲到这里,下一节我们讲讲"参数化测试"







发布于: 2020 年 06 月 06 日阅读数: 70
用户头像

高级架构师,技术顾问,交流公号:编程道与术 2020.04.28 加入

杨宇于2020年创立编程道与术,致力于研究领域分析与建模、测试驱动开发、架构设计、自动化构建和持续集成、敏捷开发方法论、微服务、云计算等顶尖技术领域。 了解更多公众号:编程道与术

评论

发布
暂无评论
原创 | TDD工具集:JUnit、AssertJ和Mockito (十九)编写测试-依赖注入\测试接口\重复测试