第27章 测试与调试:从“能跑”到“靠谱”
说实话,刚入行那会儿,我觉得测试就是浪费时间。代码写完,跑一下,界面能动,不就完事了吗?直到有一次,我发布了一个版本,结果用户反馈说“点击保存按钮,有时候会闪退”。我查了三天,最后发现是一个极其隐蔽的空指针——只在特定网络延迟下才会触发。从那以后,我彻底改变了看法。
测试不是给老板看的KPI,它是你代码的“安全带”。这一章,我就带你系统过一遍Android开发中常用的测试与调试手段。内容不少,但都是实战干货。
核心观点:测试不是为了证明代码没有bug,而是为了快速发现bug。调试则是定位bug的“手术刀”。两者缺一不可。
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 实战:一个完整的测试流程
光说不练假把式。我带你走一遍我平时是怎么测试一个登录功能的。
- 写单元测试:用JUnit + Mockito测试LoginViewModel的逻辑,包括输入校验、网络请求、状态更新。
- 写UI测试:用Espresso测试登录界面的交互——输入账号密码、点击登录、等待结果。
- 跑一遍ADB命令:用adb install装包,用adb logcat监控日志。
- 分析Logcat:如果测试失败,看日志定位问题。比如“网络超时”还是“参数错误”。
- 修复后重跑:改完代码,再跑一遍所有测试。确保没引入新bug。
记住:测试不是一次性的。每次提交代码前,至少跑一遍单元测试。UI测试可以放在CI(持续集成)里自动跑。这样你才能睡得安稳。
好了,这一章的内容就到这里。测试和调试是Android开发的“内功”,练好了,你写代码会越来越有底气。别怕麻烦,多写测试,多分析日志,你会感谢自己的。
公众号:蓝海资料掘金营,微信deep3321