第27章 测试与调试:从“能跑”到“靠谱”

说实话,刚入行那会儿,我觉得测试就是浪费时间。代码写完,跑一下,界面能动,不就完事了吗?直到有一次,我发布了一个版本,结果用户反馈说“点击保存按钮,有时候会闪退”。我查了三天,最后发现是一个极其隐蔽的空指针——只在特定网络延迟下才会触发。从那以后,我彻底改变了看法。

测试不是给老板看的KPI,它是你代码的“安全带”。这一章,我就带你系统过一遍Android开发中常用的测试与调试手段。内容不少,但都是实战干货。

核心观点:测试不是为了证明代码没有bug,而是为了快速发现bug。调试则是定位bug的“手术刀”。两者缺一不可。

Android 测试与调试知识体系 测试与调试 单元测试 UI测试 (Espresso) UI Automator ADB 常用命令 Logcat 日志分析 JUnit / Mockito Espresso UI Automator ADB Logcat

27.1 单元测试:给代码上“保险”

单元测试,说白了就是测试你写的每一个方法、每一个函数。它不关心界面长什么样,只关心逻辑对不对。

27.1.1 JUnit 基础

JUnit是Java世界最经典的单元测试框架。在Android项目里,它跑在本地JVM上,速度飞快。

// 一个简单的JUnit测试
import org.junit.Test;
import static org.junit.Assert.*;

public class CalculatorTest {
    
    @Test
    public void addition_isCorrect() {
        Calculator calc = new Calculator();
        int result = calc.add(2, 3);
        assertEquals("2 + 3 应该等于 5", 5, result);
    }
    
    @Test(expected = IllegalArgumentException.class)
    public void division_byZero_throwsException() {
        Calculator calc = new Calculator();
        calc.divide(10, 0);  // 期望抛出异常
    }
}

我的习惯:每个业务方法至少写一个“正常路径”测试和一个“异常路径”测试。别嫌多,出bug的时候你就知道值了。

27.1.2 Mockito:模拟依赖

实际项目中,一个方法往往依赖网络请求、数据库、或者系统服务。你不可能在单元测试里真的去联网。这时候,Mockito就派上用场了。

// 使用Mockito模拟依赖
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;

public class LoginViewModelTest {
    
    @Mock
    private LoginRepository mockRepository;
    
    private LoginViewModel viewModel;
    
    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        viewModel = new LoginViewModel(mockRepository);
    }
    
    @Test
    public void login_success_updatesState() {
        // 模拟网络返回成功
        when(mockRepository.login("user", "pass"))
            .thenReturn(new Result.Success(new User("user")));
        
        viewModel.login("user", "pass");
        
        // 验证状态更新
        assertEquals(LoginState.SUCCESS, viewModel.getState().getValue());
    }
}

我曾经踩过的坑:Mockito的when()方法里,参数一定要和实际调用完全一致。有一次我传了"user"但实际调用传的是"user "(多了一个空格),结果mock死活不生效,查了我两个小时。

27.2 UI测试:让界面自己“动”起来

单元测试保证了逻辑正确,但界面交互呢?按钮点没点对?列表滑没滑到位?这些就得靠UI测试了。

27.2.1 Espresso:精准操控UI

Espresso是Google官方推荐的UI测试框架。它的核心思想是:找到控件,执行操作,验证结果

// Espresso UI测试示例
import androidx.test.espresso.Espresso;
import androidx.test.espresso.action.ViewActions;
import androidx.test.espresso.matcher.ViewMatchers;
import androidx.test.ext.junit.rules.ActivityScenarioRule;

@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
    
    @Rule
    public ActivityScenarioRule<MainActivity> activityRule = 
        new ActivityScenarioRule<>(MainActivity.class);
    
    @Test
    public void buttonClick_showsText() {
        // 找到按钮并点击
        Espresso.onView(ViewMatchers.withId(R.id.btn_submit))
            .perform(ViewActions.click());
        
        // 验证TextView显示了预期文本
        Espresso.onView(ViewMatchers.withId(R.id.tv_result))
            .check(ViewAssertions.matches(
                ViewMatchers.withText("提交成功")));
    }
}

关键点:Espresso会自动等待UI线程空闲后再执行操作。所以你不需要手动加Thread.sleep()。这一点比老式的Instrumentation测试强太多了。

27.2.2 UI Automator:跨应用测试

Espresso只能测试你当前App的内部。如果你想测试“点击分享按钮后,微信朋友圈弹出来了”,那就得用UI Automator。

// UI Automator 跨应用测试
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
import androidx.test.uiautomator.UiSelector;

public class ShareTest {
    
    @Test
    public void shareToWeChat() {
        UiDevice device = UiDevice.getInstance(getInstrumentation());
        
        // 点击分享按钮(假设在App内)
        UiObject shareBtn = device.findObject(
            new UiSelector().resourceId("com.example.app:id/btn_share"));
        shareBtn.click();
        
        // 等待微信界面出现
        device.wait(Until.hasObject(
            new UiSelector().text("微信")), 5000);
        
        // 点击微信图标
        UiObject wechatIcon = device.findObject(
            new UiSelector().text("微信"));
        wechatIcon.click();
    }
}

我建议:UI Automator适合做“冒烟测试”或“关键路径测试”。别用它覆盖所有细节,因为跨应用测试太不稳定了——微信一更新,你的测试可能就挂了。

27.3 ADB:调试的瑞士军刀

ADB(Android Debug Bridge)是连接电脑和手机的桥梁。很多调试工作,离开ADB寸步难行。

命令 作用 使用场景
adb devices 列出连接的设备 确认手机是否连上
adb install app.apk 安装应用 手动安装测试包
adb uninstall com.example.app 卸载应用 清理测试环境
adb logcat 查看日志 实时监控输出
adb shell dumpsys 查看系统服务状态 分析内存、电量等
adb shell am start -n ... 启动指定Activity 直接跳转到深层页面

注意:adb install 如果遇到 INSTALL_FAILED_UPDATE_INCOMPATIBLE 错误,说明手机上已经安装了签名不同的同名应用。先卸载再安装就好。

27.4 Logcat:日志里藏着真相

Logcat是Android的日志系统。你写的Log.d()、Log.e()都会出现在这里。但很多人只会看,不会“分析”。

27.4.1 过滤日志

设备一跑起来,日志刷得飞快。不加过滤,你根本找不到想看的内容。

# 按标签过滤
adb logcat -s MyAppTag

# 按优先级过滤(V:全部, D:调试, I:信息, W:警告, E:错误)
adb logcat *:E

# 组合过滤:只显示MyAppTag的错误日志
adb logcat MyAppTag:E *:S

# 保存到文件
adb logcat -d > crash_log.txt

27.4.2 分析崩溃日志

当App崩溃时,Logcat里会打印出完整的异常堆栈。你想想看,这其实就是最直接的“破案线索”。

// 典型的崩溃日志
2024-01-15 14:23:45.678 12345-12345/com.example.app E/AndroidRuntime: 
    FATAL EXCEPTION: main
    Process: com.example.app, PID: 12345
    java.lang.NullPointerException: 
        Attempt to invoke virtual method 'int java.lang.String.length()' 
        on a null object reference
        at com.example.app.MainActivity.onClick(MainActivity.java:45)
        at android.view.View.performClick(View.java:7125)
        ... 更多调用栈 ...

我的经验:看到NullPointerException别慌。先看“at”后面的第一行,那就是出问题的代码位置。比如上面这个,就是MainActivity.java的第45行。去那里看看,十有八九是某个对象没初始化。

27.5 实战:一个完整的测试流程

光说不练假把式。我带你走一遍我平时是怎么测试一个登录功能的。

  1. 写单元测试:用JUnit + Mockito测试LoginViewModel的逻辑,包括输入校验、网络请求、状态更新。
  2. 写UI测试:用Espresso测试登录界面的交互——输入账号密码、点击登录、等待结果。
  3. 跑一遍ADB命令:用adb install装包,用adb logcat监控日志。
  4. 分析Logcat:如果测试失败,看日志定位问题。比如“网络超时”还是“参数错误”。
  5. 修复后重跑:改完代码,再跑一遍所有测试。确保没引入新bug。

记住:测试不是一次性的。每次提交代码前,至少跑一遍单元测试。UI测试可以放在CI(持续集成)里自动跑。这样你才能睡得安稳。

好了,这一章的内容就到这里。测试和调试是Android开发的“内功”,练好了,你写代码会越来越有底气。别怕麻烦,多写测试,多分析日志,你会感谢自己的。


公众号:蓝海资料掘金营,微信deep3321