JUnit 测试框架详解:从实际开发、业务测试到 Java 面试高频问题

本文从实际 Java 开发的角度系统介绍 JUnit 测试框架,不仅讲解常用注解和 API,还会结合电商订单业务分析单元测试应该如何设计,以及面试中常见的测试相关问题。

核心内容包括:

  • 什么是 JUnit,为什么实际项目需要测试
  • JUnit 的核心架构与运行原理
  • JUnit 常用注解与生命周期
  • Assertions 断言机制
  • 参数化测试
  • JUnit 与 Mockito 的关系
  • 真实订单业务单元测试
  • 单元测试、集成测试与端到端测试
  • Maven 中 JUnit 的执行过程
  • 实际开发中的测试最佳实践
  • Java 面试高频问题

一、为什么软件开发需要测试?

很多刚开始学习 Java 的开发者会产生一个疑问:

程序写完之后,我自己运行一下,看看结果是否正确,不就可以了吗?为什么还要专门写测试代码?

假设我们有一个简单的方法:

java 复制代码
public int add(int a, int b) {
    return a + b;
}

手动测试当然很简单:

java 复制代码
System.out.println(add(1, 2));

运行结果:

text 复制代码
3

但是实际业务系统远远没有这么简单。

例如一个电商系统中的"创建订单"功能可能包含:

text 复制代码
用户提交订单
    ↓
校验请求参数
    ↓
检查商品库存
    ↓
计算商品金额
    ↓
计算优惠券金额
    ↓
计算最终支付金额
    ↓
扣减库存
    ↓
保存订单
    ↓
发送消息

现在业务代码修改了一个优惠券计算规则。

开发人员需要确认:

text 复制代码
普通订单是否正常?
优惠券订单是否正常?
库存不足是否正确报错?
购买数量为 0 是否正确拦截?
订单金额是否计算正确?
库存是否真的被扣减?
异常情况下订单是否会被错误保存?

如果每修改一次代码,都需要开发人员手动启动项目、准备数据、调用接口、查看数据库,那么开发效率会非常低。

于是就需要:

自动化测试。

测试代码可以自动调用业务代码,并自动判断最终结果是否符合预期。

例如:

java 复制代码
@Test
void shouldCalculateOrderAmountCorrectly() {

    BigDecimal result = orderService.calculateAmount(
            new BigDecimal("100"),
            2
    );

    assertEquals(
            new BigDecimal("200"),
            result
    );
}

以后无论代码修改多少次,只需要执行:

bash 复制代码
mvn test

就可以自动验证程序是否仍然满足原来的业务规则。


二、什么是 JUnit?

JUnit 是 Java 生态中最经典的自动化测试框架之一。

它主要用于:

text 复制代码
编写测试
管理测试
执行测试
判断测试结果
生成测试报告

最简单的 JUnit 测试:

java 复制代码
import org.junit.jupiter.api.Test;

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

class CalculatorTest {

    @Test
    void testAdd() {

        int result = 1 + 2;

        assertEquals(3, result);
    }
}

运行测试后,JUnit 会判断:

text 复制代码
expected: 3
actual:   3

两者相同:

text 复制代码
Test Passed

如果代码出现 Bug:

java 复制代码
int result = 1 + 2 + 1;

测试结果:

text 复制代码
expected: 3
actual:   4

JUnit 会告诉我们:

text 复制代码
Test Failed

因此可以把 JUnit 理解为:

JUnit 是帮助 Java 开发人员自动执行代码,并自动验证代码行为是否符合预期的测试框架。


三、JUnit 的核心价值是什么?

JUnit 的核心价值并不是"检查一个方法有没有返回正确结果"这么简单。

在真实项目中,它主要解决以下几个问题。

1. 防止代码修改破坏已有功能

例如:

text 复制代码
V1版本:
优惠券满100减20

后来产品提出新需求:

text 复制代码
V2版本:
VIP用户满100减30

开发人员修改优惠券代码后,可能导致:

text 复制代码
普通用户优惠券计算异常

如果之前已经编写测试:

text 复制代码
CouponServiceTest
├── 普通用户满100减20
├── VIP用户满100减30
├── 金额不足100不优惠
└── 优惠金额不能超过订单金额

修改代码后重新运行测试,就能快速发现:

text 复制代码
原来的普通用户逻辑被破坏了

这种测试称为:

回归测试(Regression Testing)


2. 提高重构代码的安全性

例如原来的代码:

java 复制代码
public BigDecimal calculatePrice(...) {
    // 100行代码
}

准备重构为:

java 复制代码
calculateOriginalPrice();

calculateDiscount();

calculateShippingFee();

calculateFinalPrice();

代码结构发生了巨大变化。

但是只要原来的测试全部通过,就可以在一定程度上说明:

text 复制代码
内部实现发生了变化
但是外部业务行为没有发生变化

这也是自动化测试最大的价值之一。


3. 让业务规则变得更加明确

例如看到下面的测试:

java 复制代码
@Test
void shouldThrowExceptionWhenStockIsInsufficient() {
}

开发人员马上就知道:

text 复制代码
库存不足 → 应该抛出异常

测试代码本身实际上也是一种:

可执行的业务文档。


四、JUnit 的核心架构

现代 JUnit 可以从三个核心部分理解:

text 复制代码
JUnit Platform
      │
      ├── JUnit Jupiter
      │
      └── JUnit Vintage

1. JUnit Platform

JUnit Platform 是整个测试系统运行的基础平台。

可以简单理解为:

text 复制代码
JUnit Platform
      ↓
发现测试
      ↓
选择测试引擎
      ↓
执行测试
      ↓
返回测试结果

IDEA、Maven、Gradle 等工具都可以通过 JUnit Platform 执行测试。


2. JUnit Jupiter

JUnit Jupiter 是现代 JUnit 最核心的部分。

开发人员平时编写的:

java 复制代码
@Test
@BeforeEach
@AfterEach
@ParameterizedTest
@ExtendWith

基本都属于 Jupiter 编程模型。

例如:

java 复制代码
import org.junit.jupiter.api.Test;

注意包名:

text 复制代码
org.junit.jupiter

看到这个包名,通常意味着使用的是:

text 复制代码
JUnit Jupiter

3. JUnit Vintage

JUnit Vintage 主要用于兼容旧的 JUnit 3 和 JUnit 4 测试。

例如老项目中可能存在:

java 复制代码
import org.junit.Test;

而现代 Jupiter 通常是:

java 复制代码
import org.junit.jupiter.api.Test;

Vintage 可以帮助老项目逐步迁移。

不过在新项目中,一般应该优先使用:

text 复制代码
JUnit Jupiter

五、在 Maven 项目中引入 JUnit

标准 Maven 项目结构:

text 复制代码
project
│
├── src
│   ├── main
│   │   └── java
│   │
│   └── test
│       └── java
│
└── pom.xml

业务代码:

text 复制代码
src/main/java

测试代码:

text 复制代码
src/test/java

例如:

text 复制代码
src
├── main
│   └── java
│       └── com
│           └── ken
│               └── service
│                   └── OrderService.java
│
└── test
    └── java
        └── com
            └── ken
                └── service
                    └── OrderServiceTest.java

通常推荐:

text 复制代码
生产类:OrderService
测试类:OrderServiceTest

普通 Maven 项目可以添加:

xml 复制代码
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.1.1</version>
    <scope>test</scope>
</dependency>

其中:

xml 复制代码
<scope>test</scope>

表示:

text 复制代码
JUnit 依赖只在测试阶段使用
不会进入正式业务代码

对于使用 Spring Boot 的项目,一般通过:

xml 复制代码
spring-boot-starter-test

统一管理测试相关依赖,而不是随意单独修改 JUnit 版本。


六、第一个 JUnit 测试

业务代码:

java 复制代码
public class Calculator {

    public int add(int a, int b) {
        return a + b;
    }

}

测试代码:

java 复制代码
import org.junit.jupiter.api.Test;

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

class CalculatorTest {

    @Test
    void testAdd() {

        Calculator calculator = new Calculator();

        int result = calculator.add(10, 20);

        assertEquals(30, result);
    }

}

整个测试可以拆成三个阶段。


七、AAA 测试模式

实际开发中非常推荐使用:

text 复制代码
Arrange
Act
Assert

简称:

text 复制代码
AAA

Arrange:准备测试数据

java 复制代码
Calculator calculator = new Calculator();

Act:执行被测试代码

java 复制代码
int result = calculator.add(10, 20);

Assert:验证结果

java 复制代码
assertEquals(30, result);

完整代码:

java 复制代码
@Test
void testAdd() {

    // Arrange
    Calculator calculator = new Calculator();

    // Act
    int result = calculator.add(10, 20);

    // Assert
    assertEquals(30, result);
}

八、Given-When-Then 模式

在业务测试中,还有一种更加常见的写法:

text 复制代码
Given
When
Then

例如:

java 复制代码
@Test
void shouldCreateOrderSuccessfully() {

    // Given
    Long productId = 1001L;
    int quantity = 2;

    // When
    Order order = orderService.createOrder(productId, quantity);

    // Then
    assertNotNull(order);
}

含义:

text 复制代码
Given
给定什么业务条件

When
发生什么操作

Then
应该产生什么结果

在业务代码中,Given-When-Then 往往比 AAA 更容易表达业务语义。


九、JUnit 常用注解

1. @Test

表示当前方法是一个测试方法。

java 复制代码
@Test
void testCreateOrder() {

}

JUnit 会自动发现并执行这个方法。


2. @BeforeEach

每个测试方法执行之前运行。

java 复制代码
@BeforeEach
void setUp() {

    System.out.println("初始化测试数据");

}

例如:

java 复制代码
class UserServiceTest {

    private UserService userService;

    @BeforeEach
    void setUp() {
        userService = new UserService();
    }

    @Test
    void testRegister() {

    }

    @Test
    void testLogin() {

    }

}

执行顺序:

text 复制代码
setUp
testRegister

setUp
testLogin

常见用途:

text 复制代码
创建测试对象
初始化公共测试数据
初始化 Mock
准备测试环境

3. @AfterEach

每个测试执行完成后运行。

java 复制代码
@AfterEach
void tearDown() {

    System.out.println("清理测试数据");

}

执行过程:

text 复制代码
@BeforeEach
    ↓
@Test
    ↓
@AfterEach

常见用途:

text 复制代码
释放资源
删除临时文件
清理测试数据
关闭连接

4. @BeforeAll

整个测试类执行之前,只运行一次。

java 复制代码
@BeforeAll
static void beforeAll() {

}

例如:

text 复制代码
beforeAll

testA
testB
testC

只执行一次。


5. @AfterAll

整个测试类执行结束后执行一次。

java 复制代码
@AfterAll
static void afterAll() {

}

完整生命周期:

text 复制代码
@BeforeAll

    @BeforeEach
    @Test
    @AfterEach

    @BeforeEach
    @Test
    @AfterEach

@AfterAll

十、@BeforeEach 和 @BeforeAll 有什么区别?

这是非常经典的面试问题。

@BeforeEach

text 复制代码
每执行一个测试方法
就执行一次

适合:

text 复制代码
每个测试都需要一个全新的测试对象
每个测试之间不能共享状态

@BeforeAll

text 复制代码
整个测试类只执行一次

适合:

text 复制代码
启动成本较高的公共资源

例如:

text 复制代码
初始化大型测试环境
创建共享资源
准备一次性数据

实际开发中,应优先保证:

测试之间彼此独立。

因此业务单元测试通常更常使用:

java 复制代码
@BeforeEach

十一、JUnit 中的断言 Assertions

测试的核心不是:

text 复制代码
代码有没有执行

而是:

text 复制代码
执行结果是否符合预期

因此 JUnit 提供了:

text 复制代码
Assertions

1. assertEquals

判断两个值是否相等。

java 复制代码
assertEquals(100, result);

完整形式:

java 复制代码
assertEquals(expected, actual);

注意顺序:

text 复制代码
expected
actual

例如:

java 复制代码
assertEquals(100, order.getAmount());

2. assertNotEquals

判断两个值不相等。

java 复制代码
assertNotEquals(0, result);

3. assertTrue

java 复制代码
assertTrue(user.isActive());

4. assertFalse

java 复制代码
assertFalse(order.isDeleted());

5. assertNull

java 复制代码
assertNull(user.getDeletedTime());

6. assertNotNull

java 复制代码
assertNotNull(order);

7. assertSame

判断是否为:

text 复制代码
同一个对象
java 复制代码
assertSame(object1, object2);

它比较的是:

text 复制代码
对象引用

而不是单纯比较:

text 复制代码
对象内容

十二、异常测试:assertThrows

真实业务中,异常也是一种正常的业务结果。

例如:

text 复制代码
库存不足
用户不存在
余额不足
优惠券过期
订单不存在

业务代码:

java 复制代码
public void buy(int stock) {

    if (stock <= 0) {
        throw new IllegalStateException("库存不足");
    }

}

测试:

java 复制代码
@Test
void shouldThrowExceptionWhenStockIsInsufficient() {

    IllegalStateException exception =
            assertThrows(
                    IllegalStateException.class,
                    () -> service.buy(0)
            );

    assertEquals(
            "库存不足",
            exception.getMessage()
    );
}

这里验证了两个内容:

text 复制代码
1. 是否真的抛出了异常
2. 异常信息是否正确

十三、不要这样测试异常

错误写法:

java 复制代码
@Test
void testBuy() {

    try {

        service.buy(0);

    } catch (Exception e) {

        System.out.println("发生异常");

    }

}

问题在于:

text 复制代码
即使没有发生异常
测试也可能通过

正确方式:

java 复制代码
assertThrows(
        BusinessException.class,
        () -> service.buy(0)
);

十四、assertAll:一次验证多个结果

例如创建订单后,需要验证:

text 复制代码
订单用户
商品
数量
金额
状态

可以写:

java 复制代码
assertAll(

        () -> assertEquals(
                1001L,
                order.getUserId()
        ),

        () -> assertEquals(
                2001L,
                order.getProductId()
        ),

        () -> assertEquals(
                2,
                order.getQuantity()
        ),

        () -> assertEquals(
                OrderStatus.CREATED,
                order.getStatus()
        )

);

相比连续调用多个断言:

java 复制代码
assertEquals(...);
assertEquals(...);
assertEquals(...);

assertAll 可以将多个相关验证组织在一起。


十五、参数化测试

假设业务规则:

text 复制代码
购买数量必须大于 0

需要测试:

text 复制代码
0
-1
-10
-100

最直接的方式:

java 复制代码
@Test
void testQuantityZero() {

}

@Test
void testQuantityMinusOne() {

}

@Test
void testQuantityMinusTen() {

}

代码重复非常严重。

JUnit 提供:

java 复制代码
@ParameterizedTest

@ValueSource

java 复制代码
@ParameterizedTest
@ValueSource(ints = {0, -1, -10, -100})
void shouldRejectInvalidQuantity(int quantity) {

    assertThrows(
            IllegalArgumentException.class,
            () -> orderService.createOrder(quantity)
    );

}

JUnit 会分别执行:

text 复制代码
quantity = 0
quantity = -1
quantity = -10
quantity = -100

相当于自动执行了:

text 复制代码
4 个测试

@CsvSource

适合多参数测试。

例如会员折扣:

text 复制代码
普通会员 100元 → 100元
VIP会员 100元 → 90元
SVIP会员 100元 → 80元

测试:

java 复制代码
@ParameterizedTest
@CsvSource({
        "NORMAL, 100, 100",
        "VIP, 100, 90",
        "SVIP, 100, 80"
})
void shouldCalculatePriceCorrectly(
        String level,
        int originalPrice,
        int expectedPrice
) {

    int actualPrice =
            priceService.calculate(
                    level,
                    originalPrice
            );

    assertEquals(
            expectedPrice,
            actualPrice
    );

}

参数化测试非常适合:

text 复制代码
边界值测试
规则测试
不同用户类型
不同订单状态
不同输入组合

十六、@DisplayName:让测试结果更容易阅读

例如:

java 复制代码
@Test
@DisplayName("库存不足时创建订单应该失败")
void shouldFailWhenStockIsInsufficient() {

}

相比:

text 复制代码
shouldFailWhenStockIsInsufficient

测试报告中可以直接显示:

text 复制代码
库存不足时创建订单应该失败

大型项目中非常有利于:

text 复制代码
查看测试报告
定位失败用例
理解业务规则

十七、测试方法应该如何命名?

不推荐:

java 复制代码
test1()
test2()
testOrder()

因为无法表达:

text 复制代码
测试什么?
什么情况下?
预期结果是什么?

推荐:

java 复制代码
shouldCreateOrderSuccessfully()

或者:

java 复制代码
shouldThrowExceptionWhenStockIsInsufficient()

一种常见命名规则:

text 复制代码
should + 预期行为 + when + 条件

例如:

java 复制代码
shouldRejectRequestWhenQuantityIsZero()
java 复制代码
shouldApplyDiscountWhenCouponIsValid()
java 复制代码
shouldSaveOrderWhenStockIsEnough()

测试方法的名字应该尽可能表达:

业务行为。


十八、真实业务案例:订单创建

下面通过一个真实业务场景理解 JUnit。

业务需求:

text 复制代码
用户创建订单

1. 购买数量必须大于 0
2. 检查商品库存
3. 库存不足禁止下单
4. 计算订单原始金额
5. 使用优惠券计算优惠金额
6. 计算最终支付金额
7. 保存订单
8. 扣减库存

业务代码:

java 复制代码
public class OrderService {

    private final InventoryService inventoryService;
    private final CouponService couponService;
    private final OrderRepository orderRepository;

    public OrderService(
            InventoryService inventoryService,
            CouponService couponService,
            OrderRepository orderRepository
    ) {

        this.inventoryService = inventoryService;
        this.couponService = couponService;
        this.orderRepository = orderRepository;
    }

    public Order createOrder(
            Long userId,
            Long productId,
            int quantity,
            BigDecimal unitPrice,
            String couponCode
    ) {

        if (quantity <= 0) {
            throw new IllegalArgumentException(
                    "购买数量必须大于0"
            );
        }

        boolean hasStock =
                inventoryService.hasStock(
                        productId,
                        quantity
                );

        if (!hasStock) {
            throw new BusinessException(
                    "库存不足"
            );
        }

        BigDecimal originalAmount =
                unitPrice.multiply(
                        BigDecimal.valueOf(quantity)
                );

        BigDecimal discount =
                BigDecimal.ZERO;

        if (couponCode != null) {

            discount =
                    couponService.calculateDiscount(
                            couponCode,
                            originalAmount
                    );
        }

        BigDecimal payAmount =
                originalAmount.subtract(discount);

        Order order = new Order(
                userId,
                productId,
                quantity,
                payAmount
        );

        orderRepository.save(order);

        inventoryService.deduct(
                productId,
                quantity
        );

        return order;
    }

}

现在出现了一个问题:

测试 OrderService 时,需要:

text 复制代码
InventoryService
CouponService
OrderRepository

难道测试的时候真的连接:

text 复制代码
库存系统?
数据库?
优惠券系统?

如果这样做:

text 复制代码
测试速度慢
环境依赖严重
测试数据难维护
测试结果不稳定

因此就需要:

Mock。


十九、JUnit 和 Mockito 有什么区别?

这是 Java 面试中的经典问题。

JUnit

主要负责:

text 复制代码
组织测试
发现测试
执行测试
生命周期管理
断言结果
扩展机制

例如:

java 复制代码
@Test
@BeforeEach
assertEquals()
assertThrows()

Mockito

主要负责:

text 复制代码
创建 Mock 对象
模拟依赖行为
验证方法调用
隔离外部依赖

例如:

java 复制代码
when(...).thenReturn(...)
java 复制代码
verify(...)

因此:

text 复制代码
JUnit
负责执行测试

Mockito
负责模拟依赖

两者经常一起使用:

text 复制代码
JUnit + Mockito

Mockito 提供 JUnit Jupiter 扩展,可以初始化 Mock 并与 Jupiter 扩展机制配合使用。


二十、使用 Mockito 测试真实业务

测试类:

java 复制代码
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private InventoryService inventoryService;

    @Mock
    private CouponService couponService;

    @Mock
    private OrderRepository orderRepository;

    @InjectMocks
    private OrderService orderService;

}

@Mock

创建模拟对象:

java 复制代码
@Mock
private InventoryService inventoryService;

这不是真正的库存服务。

它只是:

text 复制代码
假的 InventoryService

我们可以人为控制它的返回结果。


@InjectMocks

java 复制代码
@InjectMocks
private OrderService orderService;

Mockito 会尝试将:

text 复制代码
inventoryService
couponService
orderRepository

注入:

text 复制代码
orderService

这样就可以:

text 复制代码
只测试 OrderService

而不会真的访问:

text 复制代码
数据库
Redis
第三方接口
消息队列

二十一、测试场景一:正常创建订单

业务条件:

text 复制代码
商品单价:100元
购买数量:2
订单原价:200元
优惠金额:20元
最终支付:180元
库存充足

测试:

java 复制代码
@Test
void shouldCreateOrderSuccessfully() {

    // Given
    Long userId = 1L;
    Long productId = 1001L;
    int quantity = 2;

    BigDecimal unitPrice =
            new BigDecimal("100.00");

    when(
            inventoryService.hasStock(
                    productId,
                    quantity
            )
    ).thenReturn(true);

    when(
            couponService.calculateDiscount(
                    eq("VIP20"),
                    any(BigDecimal.class)
            )
    ).thenReturn(
            new BigDecimal("20.00")
    );

    // When
    Order order =
            orderService.createOrder(
                    userId,
                    productId,
                    quantity,
                    unitPrice,
                    "VIP20"
            );

    // Then
    assertNotNull(order);

    assertEquals(
            0,
            order.getPayAmount()
                    .compareTo(
                            new BigDecimal("180.00")
                    )
    );

    verify(orderRepository)
            .save(any(Order.class));

    verify(inventoryService)
            .deduct(
                    productId,
                    quantity
            );
}

这个测试实际上验证了三个层面。

第一层:业务结果

java 复制代码
assertEquals(
        0,
        order.getPayAmount()
                .compareTo(
                        new BigDecimal("180.00")
                )
);

验证:

text 复制代码
最终金额是否正确

第二层:订单是否保存

java 复制代码
verify(orderRepository)
        .save(any(Order.class));

验证:

text 复制代码
OrderRepository.save()
是否真的被调用

第三层:库存是否扣减

java 复制代码
verify(inventoryService)
        .deduct(productId, quantity);

验证:

text 复制代码
库存扣减操作是否发生

因此测试不应该只关注:

text 复制代码
返回值

还要根据业务关注:

text 复制代码
状态变化
数据库写入
外部依赖调用
消息发送

二十二、测试场景二:库存不足

业务规则:

text 复制代码
库存不足
    ↓
禁止创建订单
    ↓
不能保存订单
    ↓
不能扣减库存

测试:

java 复制代码
@Test
void shouldThrowExceptionWhenStockIsInsufficient() {

    // Given
    Long productId = 1001L;

    when(
            inventoryService.hasStock(
                    productId,
                    10
            )
    ).thenReturn(false);

    // When
    BusinessException exception =
            assertThrows(
                    BusinessException.class,
                    () -> orderService.createOrder(
                            1L,
                            productId,
                            10,
                            new BigDecimal("100"),
                            null
                    )
            );

    // Then
    assertEquals(
            "库存不足",
            exception.getMessage()
    );

    verify(
            orderRepository,
            never()
    ).save(any());

    verify(
            inventoryService,
            never()
    ).deduct(
            anyLong(),
            anyInt()
    );

}

这里有一个非常重要的测试思想:

不仅要验证"应该发生什么",还要验证"不应该发生什么"。

库存不足时:

text 复制代码
应该:
抛出异常

不应该:
保存订单
扣减库存

这类测试在支付、订单、库存等业务中非常重要。


二十三、测试场景三:非法购买数量

业务规则:

text 复制代码
quantity <= 0

应该:

text 复制代码
直接拒绝请求

参数化测试:

java 复制代码
@ParameterizedTest
@ValueSource(ints = {
        0,
        -1,
        -10
})
void shouldRejectInvalidQuantity(
        int quantity
) {

    assertThrows(
            IllegalArgumentException.class,
            () -> orderService.createOrder(
                    1L,
                    1001L,
                    quantity,
                    new BigDecimal("100"),
                    null
            )
    );

    verifyNoInteractions(
            inventoryService,
            couponService,
            orderRepository
    );

}

为什么要验证:

java 复制代码
verifyNoInteractions(...)

因为参数本身已经非法:

text 复制代码
程序应该立即失败

不应该继续:

text 复制代码
查询库存
查询优惠券
访问数据库

二十四、什么应该 Mock?

这是非常重要的工程问题。

一般来说,应该 Mock:

text 复制代码
数据库 Repository
第三方 HTTP API
Redis
消息队列
支付服务
短信服务
邮件服务
文件系统
外部微服务
耗时服务
不稳定服务

例如测试:

text 复制代码
OrderService

可以 Mock:

text 复制代码
OrderRepository
InventoryClient
CouponService
PaymentClient

二十五、什么不应该随便 Mock?

通常不建议 Mock:

text 复制代码
简单实体类
DTO
VO
普通 Java 对象
简单工具类
当前真正需要测试的对象

例如:

java 复制代码
Order order = mock(Order.class);

很多时候没有必要。

直接:

java 复制代码
Order order = new Order();

即可。

一个重要原则:

Mock 系统边界,而不是 Mock 一切。

Mock 太多会导致:

text 复制代码
测试和实现细节严重绑定
代码稍微重构
测试就大量失败

二十六、单元测试和集成测试有什么区别?

单元测试 Unit Test

测试:

text 复制代码
一个方法
一个类
一个业务组件

例如:

text 复制代码
OrderServiceTest

通过 Mock:

text 复制代码
数据库
Redis
第三方接口

特点:

text 复制代码
速度快
隔离性强
定位问题容易

集成测试 Integration Test

测试多个组件是否能够正确协作。

例如:

text 复制代码
OrderService
    ↓
OrderRepository
    ↓
MySQL

测试:

text 复制代码
Java代码
数据库连接
SQL
ORM映射
事务

例如:

java 复制代码
@SpringBootTest
class OrderServiceIntegrationTest {

}

两者区别

对比 单元测试 集成测试
测试范围 单个类或组件 多个组件
数据库 通常 Mock 可能真实连接
Spring 容器 通常不启动 可能启动
运行速度 较慢
问题定位 容易 相对复杂
目的 验证内部业务逻辑 验证组件协作

实际项目中:

text 复制代码
不是二选一

而是:

text 复制代码
单元测试 + 集成测试

共同存在。


二十七、@SpringBootTest 和普通 JUnit 测试有什么区别?

例如:

java 复制代码
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

}

主要是:

text 复制代码
JUnit + Mockito

特点:

text 复制代码
不启动完整 Spring 容器
执行速度快
适合 Service 业务逻辑测试

而:

java 复制代码
@SpringBootTest
class OrderServiceTest {

}

通常会:

text 复制代码
启动 Spring ApplicationContext
加载 Bean
进行依赖注入

适合:

text 复制代码
集成测试

面试回答可以概括为:

@ExtendWith(MockitoExtension.class) 更适合隔离性的单元测试,而 @SpringBootTest 更适合验证多个 Spring Bean 协同工作的集成测试。实际项目中不应该所有测试都使用 @SpringBootTest,否则测试执行速度和维护成本都会明显增加。


二十八、测试边界值

很多 Bug 并不是出现在正常数据中,而是出现在:

text 复制代码
边界位置

例如:

java 复制代码
if (age >= 18) {

}

应该重点测试:

text 复制代码
17
18
19

而不是:

text 复制代码
20
30
40

例如库存:

text 复制代码
stock = 10

重点测试:

text 复制代码
quantity = 0
quantity = 1
quantity = 9
quantity = 10
quantity = 11

这种方法称为:

边界值分析。


二十九、测试正常路径和异常路径

一个完整的业务功能通常至少包含:

text 复制代码
Happy Path
Error Path
Boundary Case

例如订单创建。

Happy Path

text 复制代码
库存充足
参数正确
优惠券有效
创建成功

Error Path

text 复制代码
库存不足
用户不存在
优惠券失效
数据库异常

Boundary Case

text 复制代码
购买数量 = 0
购买数量 = 1
购买数量 = 最大库存
金额 = 优惠门槛

优秀的测试不会只测试:

text 复制代码
正常情况

而是关注:

text 复制代码
正常
异常
边界

三十、测试之间必须相互独立

假设:

java 复制代码
@Test
void testA() {

    user.setBalance(100);

}

然后:

java 复制代码
@Test
void testB() {

    assertEquals(
            100,
            user.getBalance()
    );

}

这意味着:

text 复制代码
testB依赖testA

如果单独运行:

text 复制代码
testB

可能失败。

这是一种非常危险的测试设计。

正确原则:

text 复制代码
每个测试
都应该可以独立执行

测试不应该依赖:

text 复制代码
其他测试执行顺序
共享可变状态
上一个测试产生的数据

三十一、为什么测试单独运行成功,一起运行失败?

这是非常经典的面试问题。

可能原因:

text 复制代码
共享 static 变量
共享数据库数据
测试没有清理资源
测试依赖执行顺序
使用全局缓存
随机数不稳定
时间相关逻辑
并发问题

例如:

java 复制代码
private static int count = 0;

不同测试共同修改:

text 复制代码
count

就可能产生:

text 复制代码
测试污染

专业术语:

Test Pollution

因此测试设计应该尽可能:

text 复制代码
独立
可重复
确定性

三十二、什么是一个好的单元测试?

可以从几个方面判断。

Fast

运行速度快。


Independent

测试之间相互独立。


Repeatable

无论执行多少次:

text 复制代码
结果一致

不能:

text 复制代码
今天成功
明天失败

Self-Validating

测试应该自动判断:

text 复制代码
成功
失败

而不是人工查看:

java 复制代码
System.out.println(result);

Timely

测试应该及时编写。

这些原则经常被总结为:

text 复制代码
FIRST

三十三、为什么 System.out.println 不是测试?

错误方式:

java 复制代码
@Test
void testCreateOrder() {

    Order order =
            orderService.createOrder();

    System.out.println(order);

}

问题:

text 复制代码
JUnit不知道结果对不对

最终还需要开发人员:

text 复制代码
人工查看

正确:

java 复制代码
assertEquals(
        OrderStatus.CREATED,
        order.getStatus()
);

测试必须:

自动判断结果。


三十四、代码覆盖率越高越好吗?

代码覆盖率可以反映:

text 复制代码
哪些代码被测试执行过

例如:

text 复制代码
Line Coverage = 80%

表示:

text 复制代码
大约80%的代码行
被测试执行过

但是:

高覆盖率不等于高质量测试。

例如:

java 复制代码
@Test
void test() {

    orderService.createOrder();

}

可能执行了大量代码。

覆盖率:

text 复制代码
很高

但是没有任何:

text 复制代码
assert

这种测试几乎没有实际价值。

因此:

text 复制代码
覆盖率
≠
测试质量

真正重要的是:

text 复制代码
是否验证关键业务规则
是否覆盖异常路径
是否覆盖边界条件
是否能够发现真实Bug

三十五、实际开发中应该重点测试什么?

并不是所有代码都值得投入完全相同的测试成本。

应该优先测试:

text 复制代码
核心业务逻辑
复杂条件判断
金额计算
优惠规则
权限判断
状态机
数据转换
异常处理
边界逻辑
容易出错的算法

例如:

text 复制代码
支付金额计算
订单状态流转
库存扣减
会员权益
优惠券规则
权限控制

这些代码的 Bug 可能直接导致:

text 复制代码
资金损失
数据错误
业务事故

因此测试价值非常高。


三十六、哪些代码测试价值相对较低?

例如:

java 复制代码
public String getName() {

    return name;

}

简单 Getter:

text 复制代码
逻辑非常简单

通常没有必要为了追求:

text 复制代码
100%覆盖率

机械地编写大量没有价值的测试。

测试应该考虑:

text 复制代码
风险
复杂度
业务价值

而不是单纯:

text 复制代码
追求数字

三十七、Maven 是如何执行 JUnit 测试的?

在 Maven 项目中执行:

bash 复制代码
mvn test

Maven 会进入:

text 复制代码
test 生命周期阶段

通常由 Maven Surefire Plugin 执行单元测试。

大致过程:

text 复制代码
读取 pom.xml
      ↓
编译 main 代码
      ↓
编译 test 代码
      ↓
Surefire 发现测试
      ↓
JUnit Platform
      ↓
JUnit Jupiter TestEngine
      ↓
执行 @Test
      ↓
执行 Assertions
      ↓
生成测试结果

Maven 官方生命周期中:

text 复制代码
validate
    ↓
compile
    ↓
test
    ↓
package
    ↓
verify
    ↓
install
    ↓
deploy

因此执行:

bash 复制代码
mvn package

并不是直接打包。

默认会先经过:

text 复制代码
compile
test
package

因此如果:

text 复制代码
JUnit测试失败

通常:

text 复制代码
package也会失败

这就是为什么企业项目中自动化测试可以成为:

text 复制代码
构建流程的一道质量门禁

JUnit Platform 可以由 Maven Surefire/Failsafe 执行,而 Maven 的默认生命周期也明确将 test 放在 package 之前。


三十八、测试在 CI/CD 中的作用

真实开发流程可能是:

text 复制代码
开发人员提交代码
        ↓
Git Push
        ↓
GitHub Actions / Jenkins
        ↓
Maven Build
        ↓
mvn test
        ↓
测试成功
        ↓
继续构建和部署

如果测试失败:

text 复制代码
停止部署

例如:

text 复制代码
订单金额测试失败
库存测试失败
用户权限测试失败

系统就不会继续发布。

因此自动化测试实际上是:

软件交付流程中的自动质量检查机制。


三十九、JUnit 4 和 JUnit Jupiter 的区别

这是面试中的经典问题。

JUnit 4

java 复制代码
import org.junit.Test;

JUnit Jupiter

java 复制代码
import org.junit.jupiter.api.Test;

常见区别:

JUnit 4 JUnit Jupiter
@Before @BeforeEach
@After @AfterEach
@BeforeClass @BeforeAll
@AfterClass @AfterAll
@Ignore @Disabled
@RunWith @ExtendWith
传统架构 Platform + TestEngine 架构

例如 Mockito:

现代 Jupiter 常见写法:

java 复制代码
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

}

旧项目中可能看到:

java 复制代码
@RunWith(MockitoJUnitRunner.class)
public class OrderServiceTest {

}

JUnit 官方目前仍提供 Vintage 作为旧测试迁移路径,但 Vintage 已被标记为应仅用于迁移阶段,新代码应优先使用原生 JUnit Platform/Jupiter 测试模型。


四十、面试题:什么是单元测试?

推荐回答:

单元测试是针对程序中最小可测试单元进行验证的自动化测试。在 Java 项目中,这个单元通常是一个类或者一个方法。单元测试通常会隔离数据库、Redis、第三方接口等外部依赖,只关注当前业务单元的行为是否正确。JUnit 负责测试的组织、执行和断言,Mockito 等 Mock 框架可以用来模拟外部依赖。


四十一、面试题:JUnit 的作用是什么?

推荐回答:

JUnit 是 Java 生态中的自动化测试框架,主要负责测试用例的定义、测试生命周期管理、测试发现和执行以及结果断言。现代 JUnit 基于 JUnit Platform,可以通过不同 TestEngine 执行测试,其中 Java 项目最常使用的是 JUnit Jupiter。


四十二、面试题:@BeforeEach 和 @BeforeAll 的区别?

推荐回答:

@BeforeEach 会在每个测试方法执行前运行一次,适合为每个测试创建独立测试环境;@BeforeAll 在整个测试类执行前只运行一次,适合初始化创建成本较高的共享资源。为了保证测试隔离性,普通业务单元测试通常更常使用 @BeforeEach


四十三、面试题:JUnit 和 Mockito 有什么区别?

推荐回答:

JUnit 是测试框架,负责测试执行、生命周期管理和断言;Mockito 是 Mock 框架,负责模拟依赖对象的行为并验证对象之间的交互。实际项目中经常使用 JUnit 执行测试,使用 Mockito 隔离数据库、远程接口等依赖。

可以总结:

text 复制代码
JUnit
解决:
怎么执行测试

Mockito
解决:
依赖对象怎么办

四十四、面试题:为什么要使用 Mock?

推荐回答:

单元测试应该尽可能只验证当前被测试对象。如果 Service 依赖数据库、Redis、第三方接口或者其他微服务,直接调用真实依赖会导致测试速度慢、环境依赖严重,并且容易受到外部系统影响。使用 Mock 可以控制依赖对象的行为,使测试更加快速、稳定和可重复。


四十五、面试题:如何测试 void 方法?

很多人认为:

java 复制代码
void方法没有返回值
无法测试

实际上可以验证:

text 复制代码
状态变化
外部交互
副作用

例如:

java 复制代码
public void cancelOrder(Long id) {

    repository.updateStatus(
            id,
            OrderStatus.CANCELLED
    );

}

测试:

java 复制代码
@Test
void shouldCancelOrder() {

    orderService.cancelOrder(1001L);

    verify(repository)
            .updateStatus(
                    1001L,
                    OrderStatus.CANCELLED
            );

}

因此:

没有返回值,不代表没有可验证的行为。


四十六、面试题:Private 方法应该怎么测试?

通常不建议直接测试:

text 复制代码
private方法

例如:

java 复制代码
private BigDecimal calculateDiscount() {

}

应该通过:

text 复制代码
public业务方法

间接验证。

例如:

text 复制代码
createOrder()

内部调用:

text 复制代码
calculateDiscount()

测试最终订单金额即可。

因为测试应该关注:

text 复制代码
外部行为

而不是过度依赖:

text 复制代码
内部实现细节

如果一个 private 方法:

text 复制代码
非常复杂
非常重要
非常难测试

往往意味着:

text 复制代码
这个方法可能应该被拆分成独立组件

四十七、面试题:为什么不应该过度 Mock?

如果测试:

text 复制代码
Mock A
Mock B
Mock C
Mock D
Mock E
Mock F

然后大量:

java 复制代码
when(...)
verify(...)

测试可能变成:

text 复制代码
验证代码内部调用过程

而不是:

text 复制代码
验证业务行为

这样一旦重构内部代码:

text 复制代码
业务行为没有变化

测试却可能大量失败。

因此一个优秀的测试应该:

text 复制代码
关注可观察的业务行为
减少对内部实现细节的依赖

四十八、面试题:测试覆盖率达到 100% 是否代表没有 Bug?

答案:

text 复制代码
不能

原因:

text 复制代码
代码执行过
≠
业务逻辑正确

例如:

java 复制代码
@Test
void test() {

    orderService.createOrder();

}

可能产生覆盖率。

但没有验证:

text 复制代码
金额
库存
订单状态
数据库写入

仍然可能存在大量 Bug。

因此覆盖率只是:

text 复制代码
测试完整度的参考指标之一

而不是:

text 复制代码
软件质量的绝对保证

四十九、面试题:测试失败一定说明业务代码有 Bug 吗?

不一定。

可能是:

text 复制代码
业务代码有Bug

也可能是:

text 复制代码
测试代码本身错误
测试数据错误
Mock配置错误
环境异常
共享状态污染
并发问题
时间依赖问题

因此测试失败后的正确处理过程应该是:

text 复制代码
分析失败原因
    ↓
确认预期业务行为
    ↓
检查生产代码
    ↓
检查测试代码

而不是:

text 复制代码
为了让测试通过
直接删除测试

五十、实际项目中的测试策略

一个相对成熟的项目通常会形成:

text 复制代码
大量单元测试
      +
适量集成测试
      +
少量端到端测试

可以理解为:

text 复制代码
          /\
         /  \
        / E2E\
       /------\
      /集成测试\
     /----------\
    /   单元测试   \
   /--------------\

越往下:

text 复制代码
数量更多
速度更快
成本更低

越往上:

text 复制代码
更接近真实系统
执行更慢
维护成本更高

因此:

text 复制代码
不能全部写成集成测试
也不能完全没有集成测试

五十一、实际开发中的推荐测试流程

假设开发:

text 复制代码
创建订单功能

可以按照下面的顺序设计测试。

第一步:列出业务规则

text 复制代码
购买数量必须大于0
库存必须充足
优惠券必须有效
金额必须正确
订单必须保存
库存必须扣减

第二步:设计测试场景

text 复制代码
正常创建订单
库存不足
数量为0
数量为负数
无优惠券
有效优惠券
优惠金额异常

第三步:确定测试边界

text 复制代码
哪些是真实对象?
哪些需要Mock?

例如:

text 复制代码
OrderService
真实对象

OrderRepository
Mock

InventoryService
Mock

CouponService
Mock

第四步:编写 Given-When-Then

text 复制代码
Given
准备业务条件

When
执行目标行为

Then
验证业务结果

第五步:验证两类结果

不仅验证:

text 复制代码
返回值

还要验证:

text 复制代码
副作用

例如:

text 复制代码
订单是否保存
库存是否扣减
消息是否发送

五十二、总结

JUnit 的核心并不是记住:

text 复制代码
@Test
@BeforeEach
assertEquals

真正重要的是理解:

如何通过自动化测试验证业务行为。

在实际开发中,一个完整的测试过程通常是:

text 复制代码
理解业务规则
      ↓
确定测试边界
      ↓
准备测试数据
      ↓
隔离外部依赖
      ↓
执行目标代码
      ↓
验证返回结果
      ↓
验证状态变化
      ↓
验证外部交互

对于 Java 开发者来说,需要重点掌握:

text 复制代码
JUnit Jupiter
Assertions
生命周期
参数化测试
异常测试
Mockito
单元测试
集成测试
测试隔离
边界值测试
CI/CD自动测试

最终需要形成一种开发思维:

写代码不仅要考虑"正常情况下能不能运行",还要考虑"异常情况下是否正确失败、边界情况下是否正确处理,以及未来代码修改后如何快速证明原有功能没有被破坏"。

这才是测试在真实软件开发中的真正价值。


面试速记版

text 复制代码
1. JUnit是什么?
Java自动化测试框架。

2. @Test作用?
标记测试方法。

3. @BeforeEach?
每个测试前执行。

4. @BeforeAll?
整个测试类执行前运行一次。

5. assertEquals?
验证预期值和实际值是否相同。

6. assertThrows?
验证代码是否抛出指定异常。

7. JUnit和Mockito区别?
JUnit负责执行测试和断言,
Mockito负责Mock依赖。

8. 什么是单元测试?
隔离外部依赖,验证单个业务单元。

9. 什么是集成测试?
验证多个组件协同工作。

10. 为什么使用Mock?
提高测试速度、稳定性和隔离性。

11. 如何测试void方法?
验证状态变化或方法交互。

12. private方法怎么测试?
通常通过public方法间接验证。

13. 覆盖率100%是否代表没有Bug?
不代表。

14. 好的测试有什么特点?
快速、独立、可重复、自动验证。

15. 测试重点是什么?
正常路径、异常路径、边界条件。
相关推荐
茯苓gao2 分钟前
机器人产业的三级火箭:卖电机、卖模组与卖整机,三种商业模式的终局推演
笔记·学习·机器人
乐观的Terry25 分钟前
1、为什么要自己造发布系统
java·spring boot·后端·spring cloud·架构
ricardo197334 分钟前
SSR / SSG 性能对比:Next.js 三种渲染模式实测数据
前端·面试
LuminousCPP35 分钟前
C 语言集中实践全记录:顺序表 + 单链表原理实现与通讯录项目实战【附可运行源码】
c语言·开发语言·数据结构·经验分享·笔记
兰令水38 分钟前
hot100【acm版】【2026.7.13打卡-java版本】
java·开发语言·数据结构·算法·leetcode·面试
马里马里奥-1 小时前
大模型应用开发笔记04:LangChain 工具(Tool)与中间件(Middleware)详解
笔记·中间件·langchain
名字还没想好☜1 小时前
用 Redis + Redisson 实现分布式锁:从踩坑到生产可用
java·redis·分布式·junit·分布式锁·redisson
谙忆1 小时前
图片CDN与边缘优化实战:缓存策略、图片处理型CDN与动态裁剪
java·spring·缓存
ltqvibe2 小时前
怎么让本体语义变成可执行的智能体
java·人工智能·本体语义
javajenius2 小时前
Masslynx 4.1安装包免费下载及详细安装教程
java·服务器·前端·其他