1. 单元测试概述
1.1 什么是单元测试
单元测试是软件开发中的一种测试方法,用于验证代码中最小可测试单元(通常是方法或类)的正确性。在 Android 开发中,单元测试主要用于:
- 验证业务逻辑:确保方法按预期工作
- 提高代码质量:发现潜在的 bug 和设计问题
- 支持重构:为代码重构提供安全网
- 文档化代码:测试用例本身就是代码的使用文档
1.2 单元测试的特点
java
@RunWith(MockitoJUnitRunner.class)
public class ExampleTest {
// 测试运行在本地 JVM 环境中,执行速度快
// 不能直接访问 Android Framework API
// 需要通过 Mock 对象模拟 Android 组件
}
核心特点:
- 快速执行:运行在 JVM 上,无需启动 Android 环境
- 隔离性强:每个测试独立运行,互不影响
- Mock 依赖:通过 Mock 对象模拟外部依赖
- 自动化:可集成到 CI/CD 流程中
2. 测试框架与工具
2.1 JUnit 4 框架
JUnit 是 Java 生态系统中最流行的单元测试框架:
java
public class JUnitBasicTest {
@BeforeClass
public static void setUpClass() {
// 【功能】在所有测试方法执行前运行一次
// 【底层原理】静态方法,类加载时执行
// 【使用场景】初始化昂贵的资源,如数据库连接
}
@Before
public void setUp() {
// 【功能】在每个测试方法执行前运行
// 【底层原理】实例方法,每次创建新的测试实例时执行
// 【使用场景】初始化测试数据和 Mock 对象
}
@Test
public void testExample() {
// 【功能】实际的测试逻辑
// 【底层原理】通过 @Test 注解标记,JUnit 运行器自动发现
assertTrue("测试条件", condition);
}
@After
public void tearDown() {
// 【功能】在每个测试方法执行后运行
// 【底层原理】无论测试成功还是失败都会执行
// 【使用场景】清理资源,重置状态
}
@AfterClass
public static void tearDownClass() {
// 【功能】在所有测试方法执行后运行一次
// 【底层原理】静态方法,类卸载前执行
// 【使用场景】释放全局资源
}
}
JUnit 注解
java
// 【功能】JUnit 注解完整演示
// 【底层原理】注解通过反射机制被 JUnit 运行器识别和处理
// 【面试高频考点】问:@Test 注解的属性有哪些?
public class JUnitAnnotationsTest {
@Test
public void basicTest() {
// 【功能】基础测试方法
// 【底层原理】JUnit 通过反射调用带有 @Test 注解的方法
}
@Test(timeout = 1000)
public void timeoutTest() {
// 【功能】测试方法执行时间限制
// 【底层原理】JUnit 在单独线程中执行,超时则中断
// 【面试高频考点】问:如何测试方法的性能?
try {
Thread.sleep(500); // 模拟耗时操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Test(expected = IllegalArgumentException.class)
public void exceptionTest() {
// 【功能】测试期望抛出的异常
// 【底层原理】JUnit 捕获异常并验证类型
throw new IllegalArgumentException("Expected exception");
}
@Ignore("暂时跳过此测试")
@Test
public void ignoredTest() {
// 【功能】跳过执行的测试
// 【底层原理】JUnit 识别 @Ignore 注解,不执行方法
fail("This test should not run");
}
@Test
@Category(SlowTests.class)
public void categorizedTest() {
// 【功能】测试分类,可选择性执行
// 【底层原理】通过 @Category 注解标记测试类型
}
}
// 测试分类接口
interface SlowTests {}
interface FastTests {}
2.2 Mockito 框架
Mockito 是 Java 中最流行的 Mock 框架:
java
// 【功能】Mockito 核心功能演示
// 【底层原理】通过字节码操作(ByteBuddy)创建代理对象
@RunWith(MockitoJUnitRunner.class)
public class MockitoBasicTest {
@Mock
private UserService mockUserService; // 【功能】创建 Mock 对象
@Spy
private UserValidator spyValidator; // 【功能】创建 Spy 对象,部分 Mock
@InjectMocks
private UserController userController; // 【功能】自动注入 Mock 依赖
@Test
public void testUserLogin() {
// 【功能】设置 Mock 对象的行为,Mockito 拦截方法调用,返回预设值
when(mockUserService.findUser("admin")).thenReturn(new User("admin"));
// 【功能】执行被测试的方法
boolean result = userController.login("admin", "password");
// 【功能】验证方法调用,Mockito 记录所有方法调用,支持验证
verify(mockUserService).findUser("admin");
verify(mockUserService, times(1)).validatePassword(any());
assertTrue(result);
}
}
2.2.1 相关注解
@Mock
创建一个"完全假的"对象,所有方法调用默认返回 null
、0
、false
等。
Mockito 使用 ByteBuddy 生成字节码代理,拦截所有方法调用,然后决定是返回默认值,还是返回你通过 when(...).thenReturn(...)
指定的值。
@Spy
创建一个"部分假的"对象,会调用真实方法,除非你对某个方法打桩。底层也是字节码代理,只是默认的行为是先执行真实方法,再看有没有覆盖逻辑。
@InjectMocks
自动实例化一个类,并把用 @Mock
或 @Spy
注解的依赖字段自动注入到这个类中。
2.2.2 相关方法
when(...).thenReturn(...)
设置 Mock 对象在调用某个方法时返回特定值。调用 when()
的时候其实不会执行真实方法 (即使是 Spy)。调用 thenReturn()
把"调用方法"与"返回值"绑定。
Mockito 会拦截方法调用,记录方法的签名,并把它和返回值存储在代理对象的内部映射表中。之后真正执行该方法时,直接查表返回,而不是走真实逻辑。
verify(...)
验证某个 Mock 对象是否按预期调用过某些方法。Mockito 在代理对象内部记录所有调用日志 (方法名、参数、次数),verify()
会去调用日志中进行匹配。
-
verify(mock).method()
→ 验证是否调用过 -
verify(mock, times(1)).method()
→ 验证调用次数 -
verify(mock, never()).method()
→ 验证从未调用过
any()
/ anyString()
/ anyInt()
参数匹配器,表示"这个参数随便匹配"。
2.3 PowerMock 框架
PowerMock 扩展了 Mockito 的功能,支持静态方法、私有方法等的 Mock:
java
// 【功能】PowerMock 高级功能演示
// 【底层原理】通过自定义类加载器修改字节码
@RunWith(PowerMockRunner.class)
@PrepareForTest({StaticUtil.class, System.class})
public class PowerMockTest {
@Test
public void testStaticMethod() {
// 【功能】Mock 静态方法
// 【底层原理】PowerMock 修改类的字节码,替换静态方法实现
PowerMockito.mockStatic(StaticUtil.class);
when(StaticUtil.getCurrentTime()).thenReturn(1000L);
// 【功能】验证静态方法调用
PowerMockito.verifyStatic(StaticUtil.class);
StaticUtil.getCurrentTime();
}
@Test
public void testPrivateMethod() throws Exception {
// 【功能】测试私有方法
// 【底层原理】通过反射机制访问私有方法
UserService userService = new UserService();
Method privateMethod = UserService.class.getDeclaredMethod("validateInternal", String.class);
privateMethod.setAccessible(true);
boolean result = (Boolean) privateMethod.invoke(userService, "test");
assertTrue(result);
}
}
3. Mock 对象与依赖注入
3.1 Mock 对象的类型
java
// 【功能】不同类型的 Mock 对象演示
// 【底层原理】Mockito 通过不同策略创建代理对象
public class MockTypesTest {
@Test
public void testMockTypes() {
// 1. Mock - 完全虚假的对象
// 【功能】创建完全虚假的对象,所有方法都返回默认值
// 【底层原理】Mockito 创建代理对象,拦截所有方法调用
List<String> mockList = mock(List.class);
when(mockList.size()).thenReturn(10);
assertEquals(10, mockList.size());
// 2. Spy - 部分 Mock 的真实对象
// 【功能】基于真实对象,只 Mock 部分方法
// 【底层原理】Mockito 包装真实对象,选择性拦截方法
List<String> spyList = spy(new ArrayList<>());
doReturn(10).when(spyList).size(); // 使用 doReturn 避免调用真实方法
spyList.add("test"); // 调用真实方法
// 3. Stub - 预设行为的对象
// 【功能】为方法预设返回值或异常
UserService stubService = mock(UserService.class);
when(stubService.findUser(anyString()))
.thenReturn(new User("default"))
.thenThrow(new RuntimeException("Error"))
.thenReturn(null);
}
}
以上代码含义:
- 首先用
mock()
创建一个完全虚假的List
对象并强制其size()
返回 10 - 接着用
spy()
包装真实的ArrayList
,通过doReturn()
屏蔽size()
的真实逻辑但仍允许正常执行add()
- 最后用
mock()
创建一个打桩的UserService
,为其findUser()
方法依次预设返回用户对象、抛出异常和返回 null
注: 打桩对象(Stub) 指的是为某个方法提前设定好返回值或行为的 Mock 对象。
4. 依赖注入模式
java
// 【功能】依赖注入在测试中的应用
// 【底层原理】通过构造函数、setter 或字段注入依赖
// 【面试高频考点】问:如何设计可测试的代码?
public class DependencyInjectionTest {
// 被测试的类 - 使用构造函数注入
public static class UserController {
private final UserService userService;
private final EmailService emailService;
// 【功能】构造函数注入,便于测试
// 【底层原理】依赖通过构造函数传入,易于 Mock
public UserController(UserService userService, EmailService emailService) {
this.userService = userService;
this.emailService = emailService;
}
public boolean registerUser(String username, String email) {
// 【功能】业务逻辑,依赖外部服务
if (userService.userExists(username)) {
return false;
}
User user = userService.createUser(username, email);
emailService.sendWelcomeEmail(user);
return true;
}
}
@Mock
private UserService mockUserService;
@Mock
private EmailService mockEmailService;
private UserController userController;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
// 【功能】手动注入 Mock 依赖
// 【底层原理】通过构造函数传入 Mock 对象
userController = new UserController(mockUserService, mockEmailService);
}
@Test
public void testRegisterUser_Success() {
// 【功能】测试成功注册用户的场景
// 【底层原理】通过 Mock 对象控制依赖的行为
String username = "newuser";
String email = "newuser@example.com";
User expectedUser = new User(username, email);
// 设置 Mock 行为
when(mockUserService.userExists(username)).thenReturn(false);
when(mockUserService.createUser(username, email)).thenReturn(expectedUser);
doNothing().when(mockEmailService).sendWelcomeEmail(expectedUser);
// 执行测试
boolean result = userController.registerUser(username, email);
// 验证结果和交互
assertTrue(result);
verify(mockUserService).userExists(username);
verify(mockUserService).createUser(username, email);
verify(mockEmailService).sendWelcomeEmail(expectedUser);
}
@Test
public void testRegisterUser_UserExists() {
// 【功能】测试用户已存在的场景
String username = "existinguser";
when(mockUserService.userExists(username)).thenReturn(true);
boolean result = userController.registerUser(username, "email@example.com");
assertFalse(result);
verify(mockUserService).userExists(username);
// 【功能】验证不应该调用的方法
verify(mockUserService, never()).createUser(anyString(), anyString());
verify(mockEmailService, never()).sendWelcomeEmail(any(User.class));
}
}
被测试类 UserController 通过构造函数注入 UserService
和 EmailService
,避免在类内部直接创建依赖对象,使其可以被 Mock,方便测试。业务逻辑 registerUser()
调用外部服务完成用户注册和邮件发送。
testRegisterUser_Success:模拟用户不存在 → 创建用户成功 → 邮件发送成功。验证返回值为 true
,并检查所有方法都被按期望调用。
testRegisterUser_UserExists:模拟用户已存在 → 不创建用户也不发邮件。验证返回值为 false
,并检查 createUser()
和 sendWelcomeEmail()
从未被调用。
5. 基础测试模式
5.1 AAA 模式 (Arrange-Act-Assert)
java
// 【功能】AAA 测试模式演示
// 【底层原理】结构化的测试组织方式,提高可读性和维护性
public class AAAPatternTest {
@Test
public void testCalculateDiscount() {
// Arrange - 准备测试数据和环境
// 【功能】设置测试所需的所有前置条件
double originalPrice = 100.0;
double discountRate = 0.1;
DiscountCalculator calculator = new DiscountCalculator();
// Act - 执行被测试的操作
// 【功能】调用被测试的方法
double result = calculator.calculateDiscount(originalPrice, discountRate);
// Assert - 验证结果
// 【功能】验证实际结果是否符合预期
assertEquals(10.0, result, 0.01);
}
@Test
public void testUserRegistration() {
// Arrange
UserService userService = new UserService();
String username = "newuser";
String email = "newuser@example.com";
// Act
User result = userService.registerUser(username, email);
// Assert
assertNotNull(result);
assertEquals(username, result.getUsername());
assertEquals(email, result.getEmail());
assertTrue(result.isActive());
}
}
5.2 Given-When-Then 模式
java
// 【功能】BDD 风格的测试模式
// 【底层原理】行为驱动开发的测试写法,更接近自然语言
public class GivenWhenThenTest {
@Test
public void shouldReturnDiscountedPriceWhenValidDiscountApplied() {
// Given - 给定条件
// 【功能】描述测试的前置条件
PriceCalculator calculator = new PriceCalculator();
Product product = new Product("Laptop", 1000.0);
Discount discount = new Discount(0.15); // 15% 折扣
// When - 当执行某个操作时
// 【功能】描述触发的行为
double finalPrice = calculator.calculateFinalPrice(product, discount);
// Then - 那么应该得到某个结果
// 【功能】描述期望的结果
assertEquals(850.0, finalPrice, 0.01);
}
@Test
public void shouldThrowExceptionWhenInvalidDiscountProvided() {
// Given
PriceCalculator calculator = new PriceCalculator();
Product product = new Product("Phone", 500.0);
Discount invalidDiscount = new Discount(-0.1); // 负折扣
// When & Then
// 【功能】验证异常抛出
assertThrows(IllegalArgumentException.class, () -> {
calculator.calculateFinalPrice(product, invalidDiscount);
});
}
}
5.3 参数化测试
java
// 【功能】参数化测试演示
// 【底层原理】JUnit 通过反射为每组参数创建测试实例
@RunWith(Parameterized.class)
public class ParameterizedTest {
private int input;
private int expected;
public ParameterizedTest(int input, int expected) {
this.input = input;
this.expected = expected;
}
@Parameterized.Parameters
public static Collection<Object[]> data() {
// 【功能】提供测试参数数据
// 【底层原理】JUnit 为每组参数创建一个测试实例
return Arrays.asList(new Object[][] {
{0, 0},
{1, 1},
{2, 4},
{3, 9},
{4, 16}
});
}
@Test
public void testSquare() {
// 【功能】使用参数进行测试
// 【底层原理】每组参数都会执行一次此测试方法
MathUtils mathUtils = new MathUtils();
assertEquals(expected, mathUtils.square(input));
}
}