Android 单元测试(一)—— 基础

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

创建一个"完全假的"对象,所有方法调用默认返回 null0false 等。

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);
    }
}

以上代码含义:

  1. 首先用 mock() 创建一个完全虚假的 List 对象并强制其 size() 返回 10
  2. 接着用 spy() 包装真实的 ArrayList,通过 doReturn() 屏蔽 size() 的真实逻辑但仍允许正常执行 add()
  3. 最后用 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 通过构造函数注入 UserServiceEmailService,避免在类内部直接创建依赖对象,使其可以被 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));
    }
}
相关推荐
慕伏白19 分钟前
【慕伏白】Android Studio 无线调试配置
android·ide·android studio
=>>漫反射=>>44 分钟前
单元测试 vs Main方法调试:何时使用哪种方式?
java·spring boot·单元测试
南北是北北1 小时前
JetPack WorkManager
面试
低调小一1 小时前
Kuikly 小白拆解系列 · 第1篇|两棵树直调(Kotlin 构建与原生承载)
android·开发语言·kotlin
跟着珅聪学java1 小时前
spring boot 整合 activiti 教程
android·java·spring
uhakadotcom2 小时前
在chrome浏览器插件之中,options.html和options.js常用来做什么事情
前端·javascript·面试
想想就想想2 小时前
线程池执行流程详解
面试
川石课堂软件测试3 小时前
全链路Controller压测负载均衡
android·运维·开发语言·python·mysql·adb·负载均衡
程序员清风3 小时前
Dubbo RPCContext存储一些通用数据,这个用手动清除吗?
java·后端·面试
南北是北北3 小时前
JetPack ViewBinding
面试