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));
    }
}
相关推荐
前端拿破轮6 小时前
从零到一开发一个Chrome插件(二)
前端·面试·github
南北是北北6 小时前
Android TexureView和SurfaceView
前端·面试
Digitally6 小时前
如何将照片从电脑传输到安卓设备
android·电脑
教程分享大师7 小时前
创维LB2002_S905L3A处理器当贝纯净版固件下载_带root权限 白色云电脑机顶盒
android
whatever who cares7 小时前
Android Activity 任务栈详解
android
idward3077 小时前
Android的USB通信 (AOA Android开放配件协议)
android·linux
且随疾风前行.7 小时前
Android Binder 驱动 - Media 服务启动流程
android·microsoft·binder
恋猫de小郭7 小时前
Flutter 真 3D 游戏引擎来了,flame_3d 了解一下
android·前端·flutter
一笑的小酒馆7 小时前
Android使用Flow+协程封装一个FlowBus
android