软件测试
测试过程按照阶段划分可以分为:
单元测试:对程序模块进行输出正确性检验
集成测试:在单元测试基础上,整合各个模块组成子系统,进行集成测试
系统测试:将整个交付所涉及的协作内容都纳入其中考虑,包含硬件、软件、接口、操作等等一系列作为一个整体,检验是否满足软件或需求说明
验收测试:在交付或者发布之前对所做的工作进行测试检验
单元测试
单元测试是阶段性测试的首要环节,也是白盒测试的一种,该内容的编写与实践可以前置在研发完成,研发在编写业务代码的时候就需要生成对应代码的单元测试。单元测试其实是针对软件中最小的测试单元来进行验证的。这里的单元就是指相关的功能子集,比如一个方法、一个类等。值得注意的是作为最低级别的测试活动,单元测试验证的对象仅限于当前测试内容,与程序其它部分内容相隔离
单元测试有以下特征:
主要功能是证明编写的代码内容与期望输出一致
最小最低级的测试内容,保证程序基本组件正常
单元测试尽量不区分类与方法,主张以过程性的方法为测试单位
专注于测试一小块的代码,保证基础功能
剥离与外部接口、存储之间的依赖,使单元测试可控
任何时间任何顺序执行单元测试都需要是成功的
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); }
@Test void testGetUserById() { 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原则
- Arrange:布置场景(mock/stub)
- Act:执行操作
- Assert:断言结果 + verify行为
@Test void testLoginSuccess() { when(authClient.verify("user", "pass")).thenReturn(true);
boolean result = loginService.login("user", "pass");
assertTrue(result); verify(authClient).verify("user", "pass"); }
|
Spring Boot+JUnit+Mockito结合使用示例
模拟一个用户注册服务的真实开发场景,进行测试
场景说明
有一个UserService
类用于注册用户,它依赖于:
UserDao
:用户数据库操作接口
EmailService
:注册成功后发送欢迎邮件
现在要测试UserService.register(user)
方法是否:
- 成功保存用户
- 发送了邮件
- 对重复注册用户抛出异常
导入依赖
<dependencies> <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> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.11.0</version> <scope>test</scope> </dependency> <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()); } }
|