软件测试

测试过程按照阶段划分可以分为:

  1. 单元测试:对程序模块进行输出正确性检验

  2. 集成测试:在单元测试基础上,整合各个模块组成子系统,进行集成测试

  3. 系统测试:将整个交付所涉及的协作内容都纳入其中考虑,包含硬件、软件、接口、操作等等一系列作为一个整体,检验是否满足软件或需求说明

  4. 验收测试:在交付或者发布之前对所做的工作进行测试检验

单元测试

单元测试是阶段性测试的首要环节,也是白盒测试的一种,该内容的编写与实践可以前置在研发完成,研发在编写业务代码的时候就需要生成对应代码的单元测试。单元测试其实是针对软件中最小的测试单元来进行验证的。这里的单元就是指相关的功能子集,比如一个方法、一个类等。值得注意的是作为最低级别的测试活动,单元测试验证的对象仅限于当前测试内容,与程序其它部分内容相隔离

单元测试有以下特征:

  1. 主要功能是证明编写的代码内容与期望输出一致

  2. 最小最低级的测试内容,保证程序基本组件正常

  3. 单元测试尽量不区分类与方法,主张以过程性的方法为测试单位

  4. 专注于测试一小块的代码,保证基础功能

  5. 剥离与外部接口、存储之间的依赖,使单元测试可控

  6. 任何时间任何顺序执行单元测试都需要是成功的

JUnit

JUnit是一个用于编写可重复测试的简单框架。它是用于单元测试框架的xUnit体系结构的一个实例

一些概念

名称 功能作用
Assert 断言方法集合
TestCase 表示一个测试案例
TestSuite 包含一组TestCase,构成一组测试
TestResult 收集测试结果

断言

JUnit的断言主要由org.junit.jupiter.api.Assertions提供

方法 含义
assertEquals(expected, actual) 判断两个值是否相等
assertNotEquals(expected, actual) 判断两个值是否不相等
assertTrue(condition) 判断一个条件是否为 true
assertFalse(condition) 判断一个条件是否为 false
assertNull(object) 判断对象是否为 null
assertNotNull(object) 判断对象是否不为 null
assertSame(expected, actual) 判断两个对象是否引用同一个实例
assertNotSame(expected, actual) 判断两个对象是否不是同一个实例
assertArrayEquals(expectedArray, actualArray) 判断两个数组内容是否完全相等
assertThrows(Exception.class, () -> code) 断言某段代码抛出指定异常
fail(“message”) 强制让测试失败

所有断言方法都可以添加第三个参数作为失败时的提示信息,便于定位错误:

assertEquals(10, result, "计算结果不符合预期");

或者使用延迟消息构造:

assertEquals(10, result, () -> "result 应为 10,但实际是 " + result);

这样只有当断言失败时才会调用lambda构造提示信息,提高性能

常用注解

注解 说明
@Test 表示一个测试方法
@BeforeEach 每个测试方法执行前执行(类似 JUnit4 的 @Before
@AfterEach 每个测试方法执行后执行
@BeforeAll 所有测试前执行一次(需为 static)
@AfterAll 所有测试后执行一次(需为 static)
@DisplayName 给测试用例起一个更可读的名字
@Disabled 暂时禁用某个测试方法

BeforeEach、BeforeAll、AfterEach、AfterAll的区别

  • BeforeEach是每个测试方法执行前执行一次,适合初始化测试对象
  • BeforeAll是只无论多少个测试方法,都只执行一次,适合加载公共资源
  • AfterEach是每个测试方法后执行一次,可以清理mock状态、删除临时文件、回滚测试数据
  • AfterAll是所有测试结束后执行一次,可以关闭数据库连接、删除全局缓存、输出整体测试报告、

示例:

import org.junit.jupiter.api.*;

public class SampleTest {

@BeforeAll
static void beforeAll() {
System.out.println("== BeforeAll:初始化数据库连接池 ==");
}

@BeforeEach
void beforeEach() {
System.out.println("-- BeforeEach:新建测试数据 --");
}

@Test
void test1() {
System.out.println("执行 test1");
}

@Test
void test2() {
System.out.println("执行 test2");
}

@AfterEach
void afterEach() {
System.out.println("-- AfterEach:清理测试数据 --");
}

@AfterAll
static void afterAll() {
System.out.println("== AfterAll:释放数据库连接池 ==");
}
}

@BeforeEach/@AfterEach和@Test一样,作用于每个测试用例,适合每个方法都需要初始化/清理的场景

@BeforeAll/@AfterAll是整个测试类级别的初始化/清理,更适合执行前后全局配置资源或统计信息的处理

Mockito

在实际开发中,我们常常需要测试某个类或方法的行为,但这些行为可能依赖于其他复杂的外部对象(如数据库、网络服务等)。为了隔离这些依赖,我们可以使用Mock对象(伪对象)。Mockito是一个用于创建mock对象的测试框架,适用于:

  • 模拟Service、DAO、远程调用等依赖
  • 断言依赖对象的调用方式
  • 编写真正“单元级”的测试,而不是集成测试

核心概念

名称 说明
Mock 模拟一个类的对象,只保留你关心的方法行为
Stub 给mock的方法设置固定返回值
Verify 验证某个方法是否被调用、调用次数、传参是否符合预期
Spy 真实对象的包装器,可部分mock,适用于部分逻辑保持原样
Injection 把mock对象注入到被测试类中,通常结合注解@InjectMocks使用

常用注解

  • @Mock:创建mock对象
  • @InjectMocks:自动将@Mock注入到被测试对象中
  • @Spy:创建spy对象(保留原逻辑,可选定mock部分)
  • @Captor:用于捕获方法调用的参数

常用方法

方法/语法 含义
mock(Class.class) 创建一个mock对象
when(xxx).thenReturn(val) 设置方法返回值(stub)
verify(obj).method() 验证方法是否被调用
doReturn(x).when(obj).method() 用于spy中设置行为(避免调用真实方法)
doThrow(ex).when(obj).method() 设置方法抛出异常
any(), eq(x) 参数匹配器,用于模糊匹配参数

示例

假设有如下业务逻辑:

public class UserService {
@Autowired
private UserDao userDao;

public User getUserById(int id) {
return userDao.findById(id);
}
}

可以用Mockito测试而不用真的访问数据库:

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.*;
import com.project.domain.User;

class UserServiceTest {
@Mock
UserDao userDao;
@InjectMocks
UserService userService;

@BeforeEach
void setup() {
MockitoAnnotations.openMocks(this); // 初始化 @Mock/@InjectMocks
}

@Test
void testGetUserById() {
// Stub 返回值
when(userDao.findById(1)).thenReturn(new User(1, "Alice"));
// 调用测试方法
User result = userService.getUserById(1);
// 验证调用和结果
verify(userDao).findById(1);
assertEquals("Alice", result.getName());
}
}

Spy和Mock的区别

对象类型 调用方法时行为 适用场景
mock() 所有方法默认无效(返回默认值) 完全隔离依赖
spy() 保留原逻辑,部分方法可被stub 测试部分真实逻辑、或已有对象的包装

调用次数验证

java复制编辑verify(userDao, times(2)).findById(1);
verify(userDao, never()).delete(anyInt());
verify(userDao, atLeastOnce()).findById(anyInt());

AAA原则

  1. Arrange:布置场景(mock/stub)
  2. Act:执行操作
  3. Assert:断言结果 + verify行为
@Test
void testLoginSuccess() {
// Arrange
when(authClient.verify("user", "pass")).thenReturn(true);

// Act
boolean result = loginService.login("user", "pass");

// Assert
assertTrue(result);
verify(authClient).verify("user", "pass");
}

Spring Boot+JUnit+Mockito结合使用示例

模拟一个用户注册服务的真实开发场景,进行测试

场景说明

有一个UserService类用于注册用户,它依赖于:

  • UserDao:用户数据库操作接口
  • EmailService:注册成功后发送欢迎邮件

现在要测试UserService.register(user)方法是否:

  1. 成功保存用户
  2. 发送了邮件
  3. 对重复注册用户抛出异常

导入依赖

<dependencies>
<!-- Spring Boot Test,包含JUnit 5、AssertJ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- Mockito Core -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>

<!-- Mockito for JUnit 5 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
</dependencies>

代码结构

用户实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long id;
private String username;
private String email;
}

UserDao接口

@Mapper
public interface UserDao {
User findByEmail(String email);
int insert(User user);
}

MailService接口(省略实现类):

public interface MailService {
void sendWelcomeEmail(User user);
}

UserService类:

@Service
public class UserServiceImpl implements UserService{
@Autowired
private UserDao userDao;
@Autowired
private MailService mailService;

public UserService(UserDao userDao, MailService mailService) {
this.userDao = userDao;
this.mailService = mailService;
}

public void register(User user) {
if (userDao.findByEmail(user.getEmail()) != null) {
throw new UserAlreadyExistsException("User already exists");
}
userDao.insert(user);
mailService.sendWelcomeEmail(user);
}
}

JUnit + Mockito测试类:

@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserDao userDao;
@Mock
private MailService mailService;
@InjectMocks
private UserService userService;

private User newUser;

@BeforeEach
void setUp() {
newUser = new User();
newUser.setEmail("test@example.com");
newUser.setPassword("securePassword");
}

@Test
void testRegisterSuccess() {
// 用户不存在
Mockito.when(userDao.findByEmail(newUser.getEmail())).thenReturn(null);

userService.register(newUser);

// 验证调用了保存方法
Mockito.verify(userDao).insert(newUser);
// 验证发送了邮件
Mockito.verify(mailService).sendWelcomeEmail(newUser);
}

@Test
void testRegister_UserAlreadyExists_ShouldThrowException() {
// 用户已存在
Mockito.when(userDao.findByEmail(newUser.getEmail())).thenReturn(newUser);

Assertions.assertThrows(UserAlreadyExistsException.class, () -> {
userService.register(newUser);
});

// 验证未保存
Mockito.verify(userDao, Mockito.never()).insert(Mockito.any());
// 验证未发送邮件
Mockito.verify(mailService, Mockito.never()).sendWelcomeEmail(Mockito.any());
}
}