JUnit 5进阶实战:从基础测试到工程化测试框架深度应用

1. 从“会写”到“写好”:JUnit 5实战进阶的核心理念

很多Java开发者都经历过这样的阶段:项目初期,为了赶进度,测试代码要么不写,要么就是几个简单的 System.out.println 。随着项目迭代,代码越来越复杂,每次修改都提心吊胆,生怕“改A坏B”。这时候,你开始学习JUnit,能写出一些基础的 @Test 方法,通过了测试,感觉心里踏实了不少。但很快,新的问题又来了:测试代码本身变得冗长、重复,维护成本甚至超过了业务代码;一些复杂的场景(比如数据库操作、HTTP调用)不知道如何模拟;团队对测试覆盖率的要求越来越高,你却感觉无从下手。

如果你正处在这个“会写但写不好”的瓶颈期,那么这篇关于JUnit 5的实战进阶指南就是为你准备的。JUnit 5远不止是一个 @Test 注解那么简单,它是一个完整的、模块化的测试框架,提供了从断言、生命周期管理到扩展模型的一整套现代化解决方案。掌握它,意味着你能将测试从“验证功能”的层面,提升到“驱动设计、保障质量、提升效率”的工程化高度。无论是应对复杂的单体应用,还是微服务架构下的集成测试,一套娴熟的JUnit 5技能都能让你和你的团队更加从容。

2. JUnit 5架构深度解析:告别JUnit 4的思维定式

在深入实战之前,我们必须先打破对JUnit的旧有认知。JUnit 5是一个全新的开始,它由三个不同子模块组成,这种架构设计带来了前所未有的灵活性和可扩展性。

2.1 JUnit Platform:测试执行的统一基石

JUnit Platform可以理解为测试引擎的“运行层”或“调度中心”。它本身不包含具体的测试发现和执行逻辑,而是定义了一套标准的API。任何实现了这套API的测试引擎(如JUnit Jupiter、JUnit Vintage、TestNG等)都可以在JUnit Platform上运行。这带来的最大好处是,你的构建工具(Maven、Gradle)和IDE(IntelliJ IDEA、Eclipse)只需要与Platform对接,就能运行各种测试框架,实现了工具链的统一。

在Maven中,你通常会这样配置surefire插件来启用JUnit Platform:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>3.0.0-M7</version>
    <configuration>
        <properties>
            <!-- 启用JUnit Platform支持 -->
            <configurationParameters>
                junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition
            </configurationParameters>
        </properties>
    </configuration>
</plugin>

这里的 configurationParameters 允许你向JUnit Platform传递各种配置参数,例如上面这行就用于全局激活所有被 @Disabled 的测试(谨慎使用,通常仅用于诊断)。Platform层还负责监听测试执行过程,并与各种报告工具集成,生成丰富的测试报告。

2.2 JUnit Jupiter:新时代的编程与扩展模型

JUnit Jupiter是我们要重点学习和使用的部分,它包含了新的编程模型(注解如 @Test , @BeforeEach )和扩展模型。它与JUnit 4最大的不同在于其扩展机制。JUnit 4的 @Rule @ClassRule 虽然强大,但设计上存在局限,比如一条规则很难同时影响多个测试生命周期阶段。

JUnit Jupiter引入了基于依赖注入的扩展模型(Extension Model)。任何实现了 Extension 接口的类,都可以通过 @ExtendWith 注解注册到测试类或方法上。框架会在测试生命周期的关键时刻(如 beforeAll , beforeEach , afterEach , afterAll )回调扩展中对应的方法。这种设计更加清晰、灵活,且支持多重扩展。

一个常见的误解是认为 @BeforeEach 就等同于JUnit 4的 @Before 。实际上,在JUnit Jupiter中, @BeforeEach 注解的方法会 在每个 @Test @RepeatedTest @ParameterizedTest @TestFactory 方法之前执行 。这意味着如果你使用了参数化测试, @BeforeEach 会为每一组参数都执行一次,这个细节在编写涉及资源初始化的测试时需要特别注意,避免不必要的开销或副作用。

2.3 JUnit Vintage:平稳过渡的兼容层

JUnit Vintage提供了一个在JUnit 5环境中运行JUnit 3或JUnit 4编写的测试的引擎。对于遗留项目迁移,这是一个不可或缺的模块。你可以在同一个项目中,让老测试和新测试并行运行。在Gradle中,你需要显式添加依赖:

testImplementation('org.junit.vintage:junit-vintage-engine:5.9.2') {
    because '允许运行JUnit 4风格的测试'
}

注意 :虽然Vintage引擎提供了兼容性,但强烈建议在新编写的测试中完全使用JUnit Jupiter。混合使用两种风格会增加理解和维护的复杂度。迁移旧测试时,也应制定计划,逐步将其重写为JUnit 5风格,以利用新特性。

3. 超越基础断言:Hamcrest与AssertJ的优雅选择

JUnit Jupiter自带的 Assertions 类(如 assertEquals , assertTrue )是基础,但在复杂断言场景下会显得笨拙。例如,你想断言一个集合包含某个元素、且大小大于1、且所有元素都满足某个条件,用原生断言写出来会是一长串嵌套的 assertTrue 和循环,可读性极差。

3.1 Hamcrest:基于匹配器的声明式断言

Hamcrest的核心思想是“匹配器(Matcher)”。它提供了一组可读性极高的匹配器,可以通过组合来表达复杂的断言条件。它的断言语句读起来更像自然语言。

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

@Test
void testWithHamcrest() {
    List<String> result = someService.getItems();

    // 断言集合包含特定元素,且大小至少为2,且每个元素都不是空字符串
    assertThat(result, hasItem("expectedItem"));
    assertThat(result, hasSize(greaterThan(1)));
    assertThat(result, everyItem(not(emptyString())));
    
    // 更优雅的链式组合(需注意Hamcrest的组合方式)
    assertThat(result, allOf(
        hasItem("expectedItem"),
        hasSize(greaterThan(1)),
        everyItem(not(emptyString()))
    ));
}

Hamcrest的强大在于其丰富的内置匹配器库( core , beans , collections , text , number 等)和易于自定义扩展的特性。当你需要断言一个对象的多个属性时, hasProperty 匹配器尤其好用。

3.2 AssertJ:流式API的终极体验

如果说Hamcrest是声明式的优雅,那么AssertJ就是命令式的流畅。它提供了真正的流式(Fluent)API,允许你将多个断言通过 . 连接起来,形成一个非常流畅的“断言链”。这种方式在IDE的代码自动补全支持下,体验极佳。

import static org.assertj.core.api.Assertions.*;

@Test
void testWithAssertJ() {
    List<String> result = someService.getItems();
    Person person = someService.getPerson();

    // 流式断言:清晰表达了“对于结果,我断言它...并且...并且...”
    assertThat(result)
        .isNotEmpty()
        .hasSizeGreaterThan(1)
        .contains("expectedItem")
        .doesNotContain("unexpectedItem")
        .allSatisfy(item -> assertThat(item).isNotBlank());

    // 对象断言同样强大
    assertThat(person)
        .isNotNull()
        .extracting(Person::getName, Person::getAge) // 提取多个属性
        .containsExactly("John Doe", 30);
        
    // 异常断言变得异常简单
    assertThatThrownBy(() -> someService.riskyOperation())
        .isInstanceOf(IllegalArgumentException.class)
        .hasMessageContaining("invalid");
}

实操心得 :在团队中,我强烈推荐使用AssertJ。它的学习曲线平缓,流式API写起来和读起来都更符合直觉,极大地减少了断言代码的认知负担。对于从JUnit 4 assertThat (Hamcrest风格) 迁移过来的团队,AssertJ的 assertThat 方法更容易接受,且功能更强大。它的另一个巨大优势是对集合、Map、Optional、日期时间等JDK常见类型提供了深度支持,断言代码几乎不需要自己写循环或条件判断。

4. 动态测试与参数化测试:数据驱动测试的艺术

静态的 @Test 方法在测试数据固定的情况下很好用,但当你的测试逻辑相同,只是输入输出数据不同时,编写大量重复的测试方法就成了噩梦。JUnit 5的 @TestFactory @ParameterizedTest 正是为了解决这个问题。

4.1 @TestFactory:运行时生成测试用例

@TestFactory 方法不是测试方法本身,而是一个工厂,它会在运行时动态生成一组 DynamicTest (动态测试)实例。这在你需要根据外部资源(文件、数据库、网络)来生成测试时特别有用。

import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;

@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
    // 假设我们从某个地方读取了测试数据
    List<TestCase> testCases = loadTestCasesFromCsv("test-data.csv");
    
    return testCases.stream()
        .map(testCase -> dynamicTest(
            "Test case: " + testCase.getInput(), // 动态测试名称
            () -> {
                // 这里是真正的测试逻辑
                String result = processor.execute(testCase.getInput());
                assertEquals(testCase.getExpectedOutput(), result);
            }
        ));
}

动态测试的显示效果非常好,每个生成的 DynamicTest 在IDE和报告里都会作为一个独立的测试项出现,失败时能精准定位到是哪个数据出了问题。但要注意, @TestFactory 方法本身不能是 private static 的,且返回的必须是 Stream , Collection , Iterable , Iterator 类型的 DynamicTest

4.2 @ParameterizedTest:强大而灵活的参数化支持

这是JUnit 5中最常用、最强大的特性之一。一个 @ParameterizedTest 方法可以接受多组参数运行多次。JUnit 5提供了多种优雅的参数来源注解。

基础用法:@ValueSource 适用于简单的值。

@ParameterizedTest
@ValueSource(strings = {"racecar", "radar", "able was I ere I saw elba"})
void testPalindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}

进阶用法:@CsvSource 与 @CsvFileSource 用于多参数测试,CSV格式非常直观。

@ParameterizedTest(name = "{0} + {1} = {2}") // 自定义显示名称,{index}代表参数索引
@CsvSource({
    "0, 1, 1",
    "1, 2, 3",
    "49, 51, 100",
    "100, -50, 50"
})
void testAddition(int first, int second, int expectedSum) {
    Calculator calculator = new Calculator();
    assertEquals(expectedSum, calculator.add(first, second));
}

// 从类路径下的CSV文件读取参数,保持测试代码整洁
@ParameterizedTest
@CsvFileSource(resources = "/test-data.csv", numLinesToSkip = 1)
void testWithCsvFileSource(String name, int age) {
    assertNotNull(name);
    assertTrue(age > 0);
}

高级用法:自定义参数提供者 @ArgumentsSource 当参数来源非常复杂(例如来自数据库、或需要动态计算)时,你可以实现 ArgumentsProvider 接口。

public class CustomArgumentsProvider implements ArgumentsProvider {
    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of(
            Arguments.of("data1", 1, LocalDate.now()),
            Arguments.of("data2", 2, LocalDate.now().plusDays(1))
            // 可以从任何地方加载数据
        );
    }
}

@ParameterizedTest
@ArgumentsSource(CustomArgumentsProvider.class)
void testWithCustomProvider(String str, int num, LocalDate date) {
    // 测试逻辑
}

注意事项 :使用 @ParameterizedTest 时,务必利用 name 属性为每次执行设置一个有意义的显示名称。默认的名称如 [1] [2] 在测试失败时几乎无法提供有效信息。良好的命名能让你一眼看出是哪组参数导致了失败。

5. 测试生命周期与依赖注入:更精细的控制

JUnit 5对测试生命周期的钩子进行了细化和标准化,并首次在测试框架中引入了对参数解析的依赖注入支持。

5.1 生命周期钩子:@BeforeAll, @BeforeEach, @AfterEach, @AfterAll

这几个注解的行为需要精确理解:

  • @BeforeAll / @AfterAll :在整个测试类 所有 测试方法执行之前/之后运行 一次 。注解的方法必须是 static 的,除非使用 @TestInstance(Lifecycle.PER_CLASS) 将测试实例生命周期改为“每类”。
  • @BeforeEach / @AfterEach :在每个 测试方法 (包括 @Test , @RepeatedTest , @ParameterizedTest , @TestFactory )执行之前/之后运行。

一个常见的陷阱是:在 @BeforeEach 中初始化了一个昂贵的资源(如数据库连接),而你的测试类中有很多个 @ParameterizedTest ,每个测试方法会因为多组参数而执行多次,导致资源被重复初始化多次,影响性能。此时,可以考虑使用 @BeforeAll 配合 @TestInstance(Lifecycle.PER_CLASS) ,或者使用 TestInfo TestReporter 参数来条件化地初始化资源。

5.2 依赖注入:TestInfo, TestReporter, 与自定义解析器

JUnit 5允许在测试构造函数和生命周期方法中注入参数。

@Test
void testInjection(TestInfo testInfo, TestReporter testReporter) {
    // TestInfo 提供当前测试的信息
    System.out.println("Display name: " + testInfo.getDisplayName());
    System.out.println("Tags: " + testInfo.getTags());
    
    // TestReporter 用于发布测试期间的附加数据,这些数据会出现在测试报告中
    testReporter.publishEntry("custom-key", "a status message");
}

更强大的是,你可以通过实现 ParameterResolver 接口来创建自定义的依赖注入。例如,为所有需要数据库连接的测试方法自动注入一个事务性的连接:

public class DatabaseConnectionResolver implements ParameterResolver {
    @Override
    public boolean supportsParameter(ParameterContext parameterContext, 
                                     ExtensionContext extensionContext) {
        return parameterContext.getParameter().getType() == Connection.class;
    }
    
    @Override
    public Object resolveParameter(ParameterContext parameterContext, 
                                   ExtensionContext extensionContext) {
        // 创建并返回一个新的数据库连接,也许来自一个连接池
        return DataSourceManager.getTransactionalConnection();
    }
}

// 在测试类上使用
@ExtendWith(DatabaseConnectionResolver.class)
class MyRepositoryTest {
    @Test
    void testWithConnection(Connection conn) { // 连接被自动注入
        // 使用conn进行测试...
    }
}

这种机制极大地提升了测试代码的整洁度和可复用性,将样板化的资源获取逻辑从测试方法中剥离。

6. 扩展模型(Extension Model)实战:打造自己的测试工具

扩展模型是JUnit 5的王牌特性,它允许你侵入测试生命周期的各个阶段,实现高度定制化的行为。你可以通过实现多个生命周期接口来创建一个功能丰富的扩展。

6.1 实现一个简单的日志扩展

假设我们想在每个测试方法执行前后打印日志,并记录执行时间。

public class TimingAndLoggingExtension implements 
        BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback {
    
    private static final Logger LOG = LoggerFactory.getLogger(TimingAndLoggingExtension.class);
    private static final String START_TIME_KEY = "startTime";
    
    // 在@BeforeEach之后,测试方法执行之前被调用
    @Override
    public void beforeTestExecution(ExtensionContext context) {
        context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod()))
               .put(START_TIME_KEY, System.currentTimeMillis());
        LOG.info("Starting execution of test: {}", context.getDisplayName());
    }
    
    // 在测试方法执行之后,@AfterEach之前被调用
    @Override
    public void afterTestExecution(ExtensionContext context) {
        Long startTime = context.getStore(ExtensionContext.Namespace.create(getClass(), context.getRequiredTestMethod()))
                                .remove(START_TIME_KEY, Long.class);
        long duration = System.currentTimeMillis() - startTime;
        LOG.info("Finished execution of test: {} | Duration: {} ms", 
                 context.getDisplayName(), duration);
        
        // 可以基于时长做一些事情,比如标记慢测试
        if (duration > 1000) {
            LOG.warn("Test {} is slow!", context.getDisplayName());
        }
    }
    
    @Override
    public void beforeEach(ExtensionContext context) {
        LOG.debug("Setting up for test: {}", context.getDisplayName());
    }
    
    @Override
    public void afterEach(ExtensionContext context) {
        LOG.debug("Tearing down after test: {}", context.getDisplayName());
    }
}

// 使用扩展
@ExtendWith(TimingAndLoggingExtension.class)
class MyServiceTest {
    // 你的测试方法...
}

这个例子展示了 ExtensionContext.Store 的用法,它是一个在特定作用域(如一个测试方法)内存储和检索数据的安全方式,用于在生命周期回调之间传递信息。

6.2 条件测试执行:@EnabledIf/@DisabledIf

虽然JUnit 5提供了 @EnabledOnOs , @DisabledIfEnvironmentVariable 等内置条件注解,但通过扩展,你可以创建更复杂的条件逻辑。

public class RunIfDatabaseAvailableCondition implements ExecutionCondition {
    @Override
    public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) {
        try {
            // 尝试连接数据库
            boolean dbAvailable = DatabaseHealthChecker.isAvailable();
            if (dbAvailable) {
                return ConditionEvaluationResult.enabled("Database is available");
            } else {
                return ConditionEvaluationResult.disabled("Database is unavailable, test disabled");
            }
        } catch (Exception e) {
            return ConditionEvaluationResult.disabled("Failed to check database status: " + e.getMessage());
        }
    }
}

// 定义自己的注解
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RunIfDatabaseAvailableCondition.class)
public @interface EnabledIfDatabaseAvailable {
}

// 使用自定义条件注解
@Test
@EnabledIfDatabaseAvailable
void testDatabaseIntegration() {
    // 这个测试只有在数据库可用时才会执行
}

通过组合不同的扩展,你可以构建出非常适合自己项目需求的测试基础设施,比如统一的事务管理、模拟数据注入、分布式锁协调等。

7. 集成测试与Mock实战:Spring Boot + Mockito的最佳拍档

单元测试通常隔离单个组件,但集成测试需要让多个组件协同工作。Spring Boot Test与JUnit 5、Mockito的集成是目前Java生态中最主流的测试方案。

7.1 Spring Boot 切片测试(Slice Test)

Spring Boot Test提供了“切片测试”的概念,让你可以只加载应用程序的某一部分,而不是整个应用上下文,从而加快测试速度。

  • @WebMvcTest : 用于测试Spring MVC控制器。它会自动配置MockMvc,并只加载 @Controller , @ControllerAdvice , @JsonComponent 等相关的Bean。非常适合测试REST API。
    @WebMvcTest(UserController.class) // 只加载UserController相关的配置
    class UserControllerTest {
        @Autowired
        private MockMvc mvc; // 自动注入
        
        @MockBean // 自动创建并注入Mock
        private UserService userService;
        
        @Test
        void getUserById() throws Exception {
            given(userService.findById(1L)).willReturn(new User(1L, "testUser"));
            
            mvc.perform(get("/api/users/1").accept(MediaType.APPLICATION_JSON))
               .andExpect(status().isOk())
               .andExpect(jsonPath("$.username").value("testUser"));
        }
    }
    
  • @DataJpaTest : 用于测试JPA仓库。它会配置一个内存数据库(如H2),并自动注入 TestEntityManager 。它默认会开启事务并在测试后回滚。
    @DataJpaTest
    class UserRepositoryTest {
        @Autowired
        private TestEntityManager entityManager; // 用于持久化测试数据
        
        @Autowired
        private UserRepository userRepository;
        
        @Test
        void whenFindByName_thenReturnUser() {
            // 给定
            User alex = new User("alex");
            entityManager.persist(alex);
            entityManager.flush();
            
            // 当
            User found = userRepository.findByUsername("alex");
            
            // 那么
            assertThat(found.getUsername()).isEqualTo(alex.getUsername());
        }
    }
    
  • @JsonTest : 专门测试JSON序列化/反序列化。
  • @RestClientTest : 用于测试REST客户端。

实操心得 :正确选择切片测试注解能极大提升测试速度。对于纯粹的单元测试(不涉及Spring容器),不要使用任何Spring Boot Test注解,直接使用Mockito即可。只有当你需要测试Spring管理的Bean之间的集成时,才使用 @SpringBootTest (加载完整上下文)或相应的切片测试。

7.2 Mockito深度使用技巧

Mockito是模拟框架的事实标准。除了基础的 when(...).thenReturn(...) ,还有一些高级技巧能让你写出更健壮、更清晰的测试。

验证交互行为 :测试不仅关心结果,有时也关心对象之间的协作方式。

@Test
void testServiceCollaboration() {
    // 给定
    User user = new User("test");
    when(userRepository.save(any(User.class))).thenReturn(user);
    when(emailService.sendWelcomeEmail(anyString())).thenReturn(true);
    
    // 当
    registrationService.registerUser("test@example.com", "password");
    
    // 那么
    // 验证userRepository的save方法被调用了一次,且参数是任意User对象
    verify(userRepository, times(1)).save(any(User.class));
    // 验证emailService的sendWelcomeEmail被调用了一次,且参数是特定的邮箱
    verify(emailService, times(1)).sendWelcomeEmail("test@example.com");
    // 验证在调用过程中,某个方法从未被调用过
    verify(notificationService, never()).sendSms(anyString());
}

参数捕获器(ArgumentCaptor) :当你想对传递给Mock方法的实际参数进行更细致的断言时,这非常有用。

@Test
void testArgumentCaptor() {
    ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
    
    registrationService.registerUser("new@example.com", "secret");
    
    verify(userRepository).save(userCaptor.capture());
    User savedUser = userCaptor.getValue();
    
    assertThat(savedUser.getEmail()).isEqualTo("new@example.com");
    assertThat(savedUser.getPassword()).isNotEqualTo("secret"); // 应该被加密了
    assertThat(savedUser.getEncryptedPassword()).isNotNull();
}

使用 @Spy 进行部分模拟 :有时候你不想完全模拟一个对象,只想模拟它的某些方法,而让其他方法保持真实行为。这时可以用 @Spy (或 spy() 方法)。

@Spy
private VeryComplexService complexService; // 真实对象,但可以被“盯梢”和“篡改”

@Test
void testWithSpy() {
    // 模拟其中一个昂贵或不可靠的方法
    doReturn("cached result").when(complexService).expensiveNetworkCall(anyString());
    
    // 其他方法保持真实行为
    String result = complexService.process("input");
    
    assertThat(result).isEqualTo("processed cached result");
    // 验证某个真实方法被调用了
    verify(complexService).someInternalMethod("input");
}

警告 :对Spy对象使用 when(...).thenReturn(...) 的语法有时会导致真实方法被意外调用(因为 when 的参数需要先执行方法)。更安全的做法是使用 doReturn(...).when(...) 的语法,如上例所示。

8. 测试代码的质量与可维护性

写出能跑的测试只是第一步,写出 的测试代码同样重要。糟糕的测试代码会成为项目的沉重负担。

8.1 测试的FIRST原则

好的单元测试应该遵循FIRST原则:

  • F ast(快速):测试应该快速执行。如果测试套件需要运行几分钟甚至几小时,开发者就不会频繁运行它,持续集成的反馈周期也会变长。
  • I ndependent(独立):测试之间不应该有依赖关系,可以以任何顺序运行。一个测试的成功或失败不应影响另一个测试。这意味着要避免共享可变的测试状态,每个测试都应该自己准备数据,并在完成后清理。
  • R epeatable(可重复):测试在任何环境中(本地开发机、CI服务器)都应该能产生相同的结果。这意味着要避免依赖外部不可控服务(如线上API)、随机数(除非特意测试随机性)或未清理的全局状态。
  • S elf-Validating(自我验证):测试应该能自动判断通过还是失败,不需要人工检查日志或输出。断言(Assertion)是测试的核心。
  • T imely(及时):理想情况下,测试代码应该与生产代码同时编写(测试驱动开发TDD)。最迟也应在代码提交前完成。事后补测试往往困难且不完整。

8.2 可读性模式:Given-When-Then

这是一种组织测试代码的经典模式,能极大提升测试的可读性。

@Test
void transferMoney_shouldSucceed_whenFundsAreSufficient() {
    // Given (准备阶段):设置测试前提、初始化对象、准备模拟数据
    Account sourceAccount = new Account("ACC-001", BigDecimal.valueOf(1000));
    Account targetAccount = new Account("ACC-002", BigDecimal.valueOf(500));
    TransferService transferService = new TransferService();
    
    // When (执行阶段):执行被测试的操作
    TransferResult result = transferService.transfer(sourceAccount, targetAccount, BigDecimal.valueOf(200));
    
    // Then (验证阶段):验证结果和行为是否符合预期
    assertThat(result.isSuccess()).isTrue();
    assertThat(sourceAccount.getBalance()).isEqualByComparingTo("800");
    assertThat(targetAccount.getBalance()).isEqualByComparingTo("700");
    // 也可以验证交互行为
    // verify(notificationService).sendTransferNotification(...);
}

清晰的注释块( // Given , // When , // Then )将测试逻辑分段,让任何阅读代码的人都能立刻理解测试的意图、场景和预期。

8.3 常见陷阱与最佳实践

  1. 避免测试私有方法 :单元测试应该关注公共API的行为。如果你觉得需要测试一个私有方法,这通常是一个信号,表明这个方法可能应该被提取到另一个类中,并拥有公共的职责。测试私有方法会使得重构变得困难,因为内部实现的改变会破坏测试。
  2. 每个测试一个断言 :这是一个指导性原则,而非铁律。目标是“每个测试验证一个概念”。如果一个测试方法里有多个断言,但它们都在验证同一件事的不同方面(例如,创建一个用户后,验证其各个字段),那是可以接受的。但如果一个测试方法在验证创建用户 发送邮件 更新日志,那就应该拆分成多个测试。
  3. 使用有意义的测试名称 :测试方法名应该清晰地表达它要测试什么,在什么条件下,期望什么结果。可以使用 methodUnderTest_scenario_expectedBehavior 的命名约定。JUnit 5的 @DisplayName 注解可以让你使用更自然语言的名字,在报告里更好看。
  4. 管理测试数据 :避免在测试方法中硬编码大量字符串和数字。考虑使用工厂方法(如 createValidUser() )、建造者模式(如 UserBuilder )或测试数据工具(如Java Faker)来生成更有意义、更随机的测试数据,这能防止测试变得脆弱,并覆盖更多边界情况。
  5. 及时清理 :如果测试修改了文件系统、数据库或静态变量,一定要在 @AfterEach @AfterAll 中恢复原状。使用 try-with-resources 或JUnit 5的 @TempDir (用于临时文件和目录)来管理资源。

从“会写测试”到成为“自动化测试高手”,关键在于观念的转变:测试不是开发完成后的一项繁琐任务,而是驱动设计、保障质量、提升开发效率的核心实践。JUnit 5提供的现代化工具链,让你能够以更优雅、更高效的方式实践这一理念。掌握生命周期管理、条件执行、动态测试、参数化测试以及强大的扩展模型,你就能构建出适应各种复杂场景的、健壮且可维护的自动化测试套件。记住,最好的测试代码应该像生产代码一样被精心设计和维护。

第1章 UNIX基本使用和基本命令.1 1.1 课程目标 1.2 UNIX概述1 1.3 UNIX SHELL.1 1.4 HP-UX的登录和注销.2 1.4.1 典型的终端会话过程2 1.4.2 登录.3 1.4.3 注销.4 1.5 命令行的格式.4 1.5.1 命令行格式4 1.5.2 二级提示符5 1.6 基本命令的使用.5 1.6.1 Man命令.5 1.6.2 date命令6 1.6.3 id命令.6 1.6.4 who命令.6 1.6.5 passwd命令.7 1.6.6 echo命令.7 1.6.7 clear命令8 1.6.8 uname命令8 1.6.9 write命令8 1.6.10 mesg命令8 1.6.11 News命令.9 1.7 实验.9 第2章管理目录和文件.11 2.1 课程目标错误!未定义书签。 2.2 文件系统和目录结构11 2.2.1 什么是文件系统11 -ii- 2.2.2 文件系统结构.11 2.2.3 路径名13 2.2.4 一些特殊的目录.14 2.3 对目录操作的基本命令.15 2.3.1 pwd——显示当前的工作目录.16 2.3.2 ls——查看目录内容.16 2.3.3 cd——改变目录17 2.3.4 mkdir和rmdir——创建和删除目录17 2.3.5 find——查找文件.17 2.4 文件操作基本命令.18 2.4.1 文件属性18 2.4.2 cat——显示文件内容.19 2.4.3 more——显示文件内容20 2.4.4 tail——显示文件尾部的内容.20 2.4.5 lp——打印.20 2.4.6 lpstat——查看打印状态.21 2.4.7 cancle——取消打印作业21 2.4.8 cp——拷贝文件21 2.4.9 mv——移动或重命名文件.22 2.4.10 ln——对文件进行链接.23 2.4.11 rm——删除文件23 2.5 实验24 第3章文件访问权限27 3.1 课程目标.错误!未定义书签。 3.2 谁有权访问文件.27 3.3 访问类型27 3.4 文件权限28 3.5 chmod——修改文件的权限.28 3.6 umask——文件权限掩码.30 3.7 touch——更新文件的时间戳.30 3.8 chown——改变文件的所有者.31 -iii- 3.9 chgrp——改变文件的所属组32 3.10 su ——切换用户标识32 3.11 newgrp命令33 3.12 实验.34 第4章 Shell的特性和功能37 4.1 课程目标错误!未定义书签。 4.2 什么是shell37 4.3 POSIX shell的特征.37 4.4 命令别名.38 4.5 文件名补齐.38 4.6 历史和命令重输39 4.7 环境变量.40 4.7.1 用户环境.40 4.7.2 设置shell变量.41 4.7.3 两个重要的变量41 4.7.4 常用的变量赋值42 4.7.5 变量的存储机制43 4.7.6 显示变量的值43 4.7.7 将本地变量转移到用户环境中44 4.8 shell替换.44 4.8.1 变量替换.45 4.8.2 命令替换.45 4.8.3 波浪号替换45 4.9 Shell启动文件46 4.9.1 登录时发生的事情46 4.9.2 Shell启动文件.47 4.10 输入输出重定向与管道48 4.10.1 输入输出重定向简介48 4.10.2 标准输入,标准输出,和标准错误.48 4.10.3 输入重定向>与>>49 4.10.4 输入重定向<50 -iv- 4.10.5 管道51 4.11 进程控制51 4.11.1 进程查看.51 4.11.2 后台进程.52 4.11.3 前台和后台作业.53 4.11.4 Kill命令.54 4.12 实验555章使用vi编辑器.59 5.1 课程目标.错误!未定义书签。 5.2 vi编辑器介绍59 5.3 启动vi.59 5.4 vi使用模式59 5.5 退出vi.60 5.6 移动光标61 5.7 删除文本61 5.8 文本替换62 5.9 复制及移动文本.62 5.10 查找和替换62 5.11 其他编辑命令63 5.12 实验63 第6章 SAM概述.65 6.1 课程目标.错误!未定义书签。 6.2 为什么使用SAM.65 6.3 在X window中使用SAM.66 6.4 在文本终端中使用SAM67 6.5 授予用户有限的 SAM 访问权限68 6.6 实验69 第7章用户和组管理71 7.1 课程目标.错误!未定义书签。 7.2 定义用户和组账号.71 7.2.1 /etc/passwd文件.71 -v- 7.2.2 /etc/group文件.73 7.3 管理用户和组.74 7.3.1 用户管理.74 7.3.2 管理组.76 7.4 实验.77 第8章配置设备文件79 8.1 课程目标错误!未定义书签。 8.2 设备和物理路径79 8.2.1 SCSI适配器.79 8.2.2 多路转接器81 8.2.3 LAN卡81 8.2.4 RAID和磁盘阵列81 8.2.5 使用ioscan查看设备地址82 8.3 设备文件.83 8.3.1 设备文件的定义83 8.3.2 设备目录的层次84 8.3.3 字符设备和块设备84 8.3.4 主号和次号85 8.4 SCSI设备文件命名规则.85 8.4.1 总体规则.85 8.4.2 磁盘设备命名规则86 8.4.3 磁带设备命名规则87 8.5 列出已安装设备87 8.5.1 使用ioscan命令87 8.5.2 使用lssf命令.87 8.6 生成设备文件.88 8.7 实验.88 第9章配置硬盘设备91 9.1 课程目标错误!未定义书签。 9.2 硬盘分区.91 9.3 整盘分区.91 -vi- 9.4 逻辑卷管理硬盘分区.92 9.4.1 物理卷92 9.4.2 卷组92 9.4.3 逻辑卷92 9.5 LVM的设备文件.93 9.5.1 物理卷的设备文件.93 9.5.2 卷组设备文件.93 9.5.3 逻辑卷的设备文件.94 9.5.4 LVM的主号和次号.94 9.6 LVM的Extents94 9.7 创建逻辑卷95 9.7.1 创建物理卷.95 9.7.2 创建卷组96 9.7.3 创建逻辑卷.97 9.8 实验98 第10章文件系统的创建和维护.101 10.1 课程目标.错误!未定义书签。 10.2 文件系统概念.101 10.2.1 什么是文件系统.101 10.2.2 文件系统类型.101 10.2.3 文件系统结构.102 10.3 文件系统创建概述.103 10.4 创建一个新的文件系统.103 10.4.1 使用命令行方式创建文件系统概述.103 10.4.2 使用newfs创建文件系统.104 10.4.3 挂起新文件系统.106 10.4.4 卸载文件系统.107 10.4.5 自动挂起文件系统.108 10.4.6 CD-ROM文件系统109 10.5 管理文件系统.110 10.5.1 监视磁盘使用情况.110 -vii- 10.5.2 收回被浪费的文件系统空间111 10.5.3 扩展一个文件系统112 10.6 文件系统修复115 10.7 实验.116 第11章系统备份恢复.121 11.1 课程目标错误!未定义书签。 11.2 备份概述.121 11.3 备份类型.121 11.3.1 完全备份122 11.3.2 增量备份122 11.4 备份和恢复的方法122 11.5 使用tar.123 11.6 使用fbackup和frecover124 11.6.1 备份单一目录124 11.6.2 使用graph文件.125 11.6.3 使用frecover.125 11.7 使用Ignite-UX.126 11.7.1 创建恢复磁带126 11.7.2 更新恢复磁带127 11.8 实验.127 第12章计划cron任务.129 12.1 课程目标错误!未定义书签。 12.2 后台守护程序129 12.3 cronfile129 12.4 用crontab管理cronfile130 12.5 当任务被调度的时候发生了什么?131 12.6 实验.131 第13章系统的关机和重起.133 13.1 课程目标错误!未定义书签。 13.2 HP-UX操作状态.133 13.3 用shutdown和reboot改变系统状态.133 -viii- 13.3.1 Shutdown命令133 13.3.2 reboot命令134 13.4 系统引导过程.134 13.4.1 系统引导过程简介.134 13.4.2 自动引导和手工引导.135 13.4.3 与PDC/BootRom交互.135 13.4.4 与ISL/IPL交互136 13.5 运行级137 13.6 实验138 第14章网络连接141 14.1 课程目标.错误!未定义书签。 14.2 网络管理基本命令.141 14.2.1 hostname命令.141 14.2.2 telnet命令142 14.2.3 ftp 命令.142 14.2.4 rlogin 命令143 14.2.5 rcp 命令.143 14.2.6 remsh 命令144 14.2.7 rwho命令144 14.2.8 ruptime 命令.145 14.3 修改和配置网络参数.145 14.3.1 设置IP地址和子网掩码.145 14.3.2 设置默认路由.146 14.3.3 解析主机名为IP地址.146 14.4 配置IP的连通性.147 14.4.1 系统启动时网络初始化文件.147 14.4.2 配置网络连通性.148 14.5 网络故障排除.149 14.5.1 网络查错工具.149 14.5.2 2.潜在的网络连接问题.150 14.5.3 arp命令151 -ix- 14.5.4 ping命令152 14.5.5 netstat -i命令.153 14.5.6 netstat -r命令.154 14.5.7 nslookup命令.156 14.6 启动或禁止网络服务156 14.6.1 internet服务的服务进程启动.156 14.6.2 配置/etc/services文件.157 14.6.3 配置/etc/inetd.conf文件.158 14.6.4 配置/var/adm/inetd.sec文件.160 14.7 实验.161 第15章 HP Cluster简介163 15.1 课程目标错误!未定义书签。 15.2 HP MC/ServiceGuard背景知识163 15.3 HP MC/ServiceGuard运行的硬件环境164 15.3.1 集群系统组件的臃余164 15.3.2 网络组件的臃余164 15.3.3 储存磁盘的臃余165 15.4 HP MC/ServiceGuard软件的工作原理和组件165 15.4.1 Cluster Manager的工作原理.166 15.4.2 Package Manager的工作原理.167 15.4.3 Network Manager的工作原理168 15.5 HP cluster的硬件配置及使用.169 15.5.1 开关机步骤169 15.5.2 HPcluster配置170 15.5.3 操作维护174
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值