当前位置:Java -> Java单元测试与测试驱动开发精通
单元测试是软件测试方法论的一种,它在隔离的情况下测试软件的各个单元或组件,以检查其是否符合预期。在Java中,它是一种必要的实践,通过它来尝试验证代码的正确性,并尝试改进代码质量。它基本上可以确保代码正常运行,且更改不会破坏现有功能。
测试驱动开发(TDD)是一种短迭代的软件开发方法,采用测试优先的方式。这种实践在真正编写源代码之前编写测试,并致力于编写通过预定义测试的代码,因此设计良好、干净、无bug。
JUnit是一种开源、简单、广泛使用的单元测试框架。JUnit是最流行的Java单元测试框架之一。换句话说,它提供了编写和运行测试所需的注解、断言和工具。
Junit使用注解来定义测试和生命周期方法。以下是一些关键注解:
@Test:
标记方法为测试方法。@BeforeEach
:表示该注解的方法应在当前类中的每个@Test
方法之前执行。@AfterEach
:表示该注解的方法应在当前类中的每个@Test
方法之后执行。@BeforeAll
:表示该注解的方法应在当前类中的任何@Test
方法之前执行一次。@AfterAll
:表示该注解的方法应在当前类中的所有@Test
方法之后执行一次。@Disabled
:用于暂时禁用测试方法或类。断言用于测试预期结果:
assertEquals
(expected, actual):断言两个值相等。如果不相等,将抛出AssertionError
。assertTrue
(boolean condition):断言条件为真。assertFalse
(boolean condition):断言条件为假。assertNotNull
(Object obj):断言对象不为空。assertThrows
(Class<T> expectedType, Executable executable):断言可执行的执行引发指定类型的异常。假设类似于断言,但在不同的上下文中使用:
assumeTrue
(boolean condition):如果条件为假,测试将终止并视为成功。assumeFalse
(boolean condition):与assumeTrue
相反。JUnit测试的生命周期从初始化到清理运行:
@BeforeAll
→ @BeforeEach
→ @Test
→ @AfterEach
→ @AfterAll
这允许适当的设置和拆卸操作,确保测试在干净的状态下运行。
以下是一个简单的JUnit测试类,用于测试基本计算器:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterEach;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@Test
void testAddition() {
assertEquals(5, calculator.add(2, 3), "2 + 3 should equal 5");
}
@Test
void testMultiplication() {
assertAll(
() -> assertEquals(6, calculator.multiply(2, 3), "2 * 3 should equal 6"),
() -> assertEquals(0, calculator.multiply(0, 5), "0 * 5 should equal 0")
);
}
@AfterEach
void tearDown() {
// Clean up resources, if necessary
calculator = null;
}
}
JUnit 5引入了一个名为动态测试的强大功能。与在编译时使用@Test
注解定义的静态测试不同,动态测试是在运行时创建的。这允许在测试创建过程中具有更多灵活性和动态性。
JUnit提供DynamicTest
类用于创建动态测试。您还需要使用@TestFactory
注解标记返回动态测试的方法。
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
class DynamicTestsExample {
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("apple", "banana", "lemon")
.map(fruit -> dynamicTest("Test for " + fruit, () -> {
assertEquals(5, fruit.length());
}));
}
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("Positive Test", () -> assertEquals(2, 1 + 1)),
dynamicTest("Negative Test", () -> assertEquals(-2, -1 + -1))
);
}
}
@ParameterizedTest注解创建参数化测试。您需要使用特定的源注解提供参数。以下是常用来源的概述:
@ValueSource
:提供单个数组的文本值。@CsvSource
:以CSV格式提供数据。@MethodSource
:从工厂方法提供数据。@EnumSource
:从枚举提供数据。@ValueSource
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ValueSourceTest {
@ParameterizedTest
@ValueSource(strings = {"apple", "banana", "orange"})
void testWithValueSource(String fruit) {
assertTrue(fruit.length() > 4);
}
}
@CsvSource
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CsvSourceTest {
@ParameterizedTest
@CsvSource({
"test,4",
"hello,5",
"JUnit,5"
})
void testWithCsvSource(String word, int expectedLength) {
assertEquals(expectedLength, word.length());
}
}
@MethodSource
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertTrue;
class MethodSourceTest {
@ParameterizedTest
@MethodSource("stringProvider")
void testWithMethodSource(String word) {
assertTrue(word.length() > 4);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana", "orange");
}
}
@DisplayName
来增加清晰度。@MethodSource
,使用提供数据集的静态方法。JUnit 5中的另一个显着特性是标记:它允许为测试分配自定义标记。因此,标记允许一种按照标记对测试进行分组并稍后选择性地执行组的方式。这对于管理大型测试套件非常有用。
要使用标记,您需要用@Tag注释注释测试方法或测试类,后跟表示标记名称的字符串。
@Tag
的示例用法import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("fast")
class FastTests {
@Test
@Tag("unit")
void fastUnitTest() {
// Test logic for a fast unit test
}
@Test
void fastIntegrationTest() {
// Test logic for a fast integration test
}
}
@Tag("slow")
class SlowTests {
@Test
@Tag("integration")
void slowIntegrationTest() {
// Test logic for a slow integration test
}
}
您可以使用以下方式运行具有特定标记的测试:
mvn test -Dgroups="fast"
JUnit 5扩展模型允许开发人员扩展和定制测试行为。它们提供了一种机制,用于通过附加功能扩展测试、修改测试执行生命周期以及向测试添加新功能。
BeforeAllCallback
、BeforeEachCallback
、AfterAllCallback
、AfterEachCallback
。ParameterResolver
。ExecutionCondition
。TestExecutionExceptionHandler
。TestInstancePostProcessor
、TestTemplateInvocationContextProvider
等。要创建自定义扩展,您需要实现上述接口中的一个或多个,并使用@ExtendWith
对类进行注释。
一个简单的参数解析器,将一个字符串注入测试方法:
import org.junit.jupiter.api.extension.*;
public class CustomParameterResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(String.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
return "Injected String";
}
}
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(CustomParameterResolver.class)
class CustomParameterTest {
@Test
void testWithCustomParameter(String injectedString) {
System.out.println(injectedString); // Output: Injected String
}
}
单元测试和测试驱动开发(TDD)带来了对软件开发过程和结果产生积极影响的显著好处。
通过在 Java 中使用 JUnit 进行单元测试和 TDD,开发人员能够生成易于维护且随着时间推移更易扩展的高质量软件。这些实践对于任何专业软件开发工作流程至关重要,有助于促进应用程序代码库的信心和稳定性。
推荐阅读: 百度面经(15)
本文链接: Java单元测试与测试驱动开发精通