ZhuanLan客户端单元测试入门:从DateConvertTest到API Mock的完整指南
ZhuanLan客户端单元测试入门从DateConvertTest到API Mock的完整指南【免费下载链接】ZhuanLan非官方知乎专栏 - Android项目地址: https://gitcode.com/gh_mirrors/zh/ZhuanLanZhuanLan是一款优秀的知乎专栏Android客户端为开发者提供了学习Android应用开发的绝佳案例。本文将带你从简单的DateConvertTest入手逐步掌握Android单元测试的核心技巧最终学会如何对网络API进行Mock测试构建健壮的移动应用测试体系。 ZhuanLan项目简介ZhuanLan是一个非官方的知乎专栏Android客户端它采用了Material Design设计风格提供了流畅的阅读体验。项目使用Retrofit进行网络请求Gson进行数据解析并实现了优雅的UI交互效果。项目核心模块网络层app/src/main/java/io/bxbxbai/zhuanlan/core/Api.java工具类app/src/main/java/io/bxbxbai/zhuanlan/utils/Utils.java数据模型app/src/main/java/io/bxbxbai/zhuanlan/bean/ 单元测试基础从DateConvertTest开始让我们从项目中的第一个测试用例开始学习。在ZhuanLan中有一个基础的单元测试类DateConvertTest它位于app/src/androidTest/java/io/bxbxbai/zhuanlan/io/bxbxbai/zhuanlan/test/DateConvertTest.java这个测试类继承自AndroidTestCase主要用于测试时间转换功能。让我们分析一下它的结构public class DateConvertTest extends AndroidTestCase { public void testDateConvert() { String str 2015-03-18T14:43:5008:00; StopWatch.log(Utils.convertPublishTime(str)); } }测试要点解析测试方法命名使用test前缀这是JUnit的命名约定测试对象测试Utils.convertPublishTime()方法输入数据使用ISO 8601格式的时间字符串验证方式通过日志输出验证结果 改进DateConvertTest添加断言原始的测试用例只输出了结果没有进行断言验证。让我们改进它添加更完整的测试逻辑public class DateConvertTest extends AndroidTestCase { public void testDateConvert() { // 测试数据 String testTime 2015-03-18T14:43:5008:00; // 执行转换 String result Utils.convertPublishTime(testTime); // 验证结果不为空 assertNotNull(转换结果不应为空, result); // 验证结果格式 assertTrue(结果应包含时间单位, result.contains(年前) || result.contains(月前) || result.contains(天前) || result.equals(今天)); } public void testWithinDays() { // 测试withinDays方法 String recentTime 2026-07-17T14:43:5008:00; String oldTime 2015-03-18T14:43:5008:00; // 验证近期时间在7天内 assertTrue(Utils.withinDays(recentTime, 7)); // 验证旧时间不在7天内 assertFalse(Utils.withinDays(oldTime, 7)); } } 理解Utils工具类的时间转换逻辑要编写有效的测试我们需要理解被测试的代码逻辑。让我们看看Utils.convertPublishTime()的实现app/src/main/java/io/bxbxbai/zhuanlan/utils/Utils.java#L35-L60这个方法的核心功能是将ISO 8601格式的时间字符串转换为相对时间描述如3天前、2月前等。它的转换逻辑基于以下时间单位时间单位秒数描述年31,536,000秒12个月 × 30天 × 24小时 × 3600秒月2,592,000秒30天 × 24小时 × 3600秒天86,400秒24小时 × 3600秒边界条件测试当天发布的内容 → 返回今天1-30天前的内容 → 返回N天前1-12月前的内容 → 返回N月前1年以上前的内容 → 返回N年前 搭建Android单元测试环境1. 项目结构配置ZhuanLan项目使用标准的Android项目结构app/ ├── src/ │ ├── main/ # 主代码 │ ├── androidTest/ # Android测试代码 │ └── test/ # 单元测试代码如有2. 依赖配置查看项目的build.gradle文件确保包含测试依赖dependencies { androidTestImplementation junit:junit:4.12 androidTestImplementation androidx.test:runner:1.4.0 androidTestImplementation androidx.test.espresso:espresso-core:3.4.0 }3. 测试运行配置在Android Studio中可以通过以下方式运行测试右键点击测试类 → Run DateConvertTest使用命令行./gradlew connectedAndroidTest API Mock测试模拟网络请求在实际开发中网络请求是最需要测试的部分。ZhuanLan使用Retrofit进行API调用让我们学习如何对这些API进行Mock测试。1. 理解API接口首先查看API接口定义 app/src/main/java/io/bxbxbai/zhuanlan/core/Api.javapublic interface Api { GET(/api/columns/{id}/posts) CallListPost getPosts(Path(id) String id, Query(limit) int limit, Query(offset) int offset); GET(/api/columns/{id}) CallUser getUser(Path(id) String id); }2. 创建Mock API测试我们可以使用Mockito库来模拟API响应public class ApiMockTest extends AndroidTestCase { private Api mockApi; Override protected void setUp() throws Exception { super.setUp(); // 创建Mock对象 mockApi Mockito.mock(Api.class); } public void testGetPostsSuccess() { // 准备模拟数据 ListPost mockPosts new ArrayList(); Post post new Post(); post.setTitle(测试文章标题); post.setPublishedTime(2026-07-18T10:00:0008:00); mockPosts.add(post); // 创建模拟的Call对象 CallListPost mockCall Mockito.mock(Call.class); ResponseListPost successResponse Response.success(mockPosts); // 设置Mock行为 Mockito.when(mockApi.getPosts(test-id, 10, 0)) .thenReturn(mockCall); Mockito.when(mockCall.execute()) .thenReturn(successResponse); // 执行测试 CallListPost call mockApi.getPosts(test-id, 10, 0); ResponseListPost response call.execute(); // 验证结果 assertTrue(response.isSuccessful()); assertEquals(1, response.body().size()); assertEquals(测试文章标题, response.body().get(0).getTitle()); } public void testGetPostsFailure() { CallListPost mockCall Mockito.mock(Call.class); Mockito.when(mockApi.getPosts(invalid-id, 10, 0)) .thenReturn(mockCall); Mockito.when(mockCall.execute()) .thenThrow(new IOException(网络错误)); // 验证异常处理 try { CallListPost call mockApi.getPosts(invalid-id, 10, 0); call.execute(); fail(应该抛出IOException); } catch (IOException e) { assertEquals(网络错误, e.getMessage()); } } }3. 使用MockWebServer进行集成测试对于更真实的网络测试可以使用OkHttp的MockWebServerpublic class ApiIntegrationTest extends AndroidTestCase { private MockWebServer server; private Api api; Override protected void setUp() throws Exception { super.setUp(); server new MockWebServer(); server.start(); Retrofit retrofit new Retrofit.Builder() .baseUrl(server.url(/).toString()) .addConverterFactory(GsonConverterFactory.create()) .build(); api retrofit.create(Api.class); } Override protected void tearDown() throws Exception { server.shutdown(); super.tearDown(); } public void testGetPostsWithMockServer() throws IOException { // 准备模拟响应 String jsonResponse [{\title\:\测试文章\,\publishedTime\:\2026-07-18T10:00:0008:00\}]; server.enqueue(new MockResponse() .setResponseCode(200) .setBody(jsonResponse) .addHeader(Content-Type, application/json)); // 执行请求 CallListPost call api.getPosts(test-column, 10, 0); ResponseListPost response call.execute(); // 验证请求 RecordedRequest request server.takeRequest(); assertEquals(/api/columns/test-column/posts?limit10offset0, request.getPath()); // 验证响应 assertTrue(response.isSuccessful()); assertEquals(1, response.body().size()); assertEquals(测试文章, response.body().get(0).getTitle()); } } 测试最佳实践1. 测试金字塔原则遵循测试金字塔原则构建健康的测试体系╱╲ ╱ ╲ E2E测试 (少量) ╱ ╲ ╱______╲ ╱ ╲ 集成测试 (适量) ╱__________╲ ╱ ╲ 单元测试 (大量) ╱______________╲2. 测试命名规范使用清晰的测试命名让测试意图一目了然test[方法名]_[场景]_[预期结果]testConvertPublishTime_RecentDate_ReturnsTodaytestGetPosts_NetworkError_ThrowsException3. 测试数据管理创建测试数据工厂避免硬编码public class TestDataFactory { public static Post createPost(String title, String time) { Post post new Post(); post.setTitle(title); post.setPublishedTime(time); post.setAuthor(createAuthor(测试作者)); return post; } public static Author createAuthor(String name) { Author author new Author(); author.setName(name); author.setAvatar(new Avatar()); return author; } }4. 测试覆盖率目标为ZhuanLan项目设定合理的测试覆盖率目标模块类型建议覆盖率优先级工具类(Utils)90%高业务逻辑80%高UI组件70%中数据模型60%低️ 常见问题与解决方案问题1Android依赖问题症状测试运行时找不到Android类解决使用androidTest目录而非test目录或使用Robolectric模拟Android环境问题2网络请求测试慢症状网络测试执行时间长解决使用MockWebServer或Mockito模拟网络层避免真实网络请求问题3异步测试困难症状异步回调难以测试解决使用CountDownLatch或Espresso的IdlingResourcepublic void testAsyncOperation() throws InterruptedException { final CountDownLatch latch new CountDownLatch(1); final ListString results new ArrayList(); api.getPosts(test, 10, 0).enqueue(new CallbackListPost() { Override public void onResponse(CallListPost call, ResponseListPost response) { results.add(success); latch.countDown(); } Override public void onFailure(CallListPost call, Throwable t) { results.add(failure); latch.countDown(); } }); // 等待异步操作完成 assertTrue(latch.await(5, TimeUnit.SECONDS)); assertEquals(success, results.get(0)); } 总结与下一步通过本文的学习你已经掌握了基础单元测试从DateConvertTest入手理解Android测试框架断言技巧使用合适的断言验证测试结果API Mock测试使用Mockito和MockWebServer模拟网络请求测试最佳实践遵循测试金字塔编写可维护的测试代码下一步学习建议探索Espresso进行UI测试学习使用Robolectric进行快速单元测试研究持续集成中的测试自动化实践测试驱动开发(TDD)方法论ZhuanLan项目为你提供了绝佳的Android开发学习案例通过系统地学习单元测试你不仅能提高代码质量还能深入理解Android应用架构。现在就开始为你的ZhuanLan客户端添加更多测试用例吧【免费下载链接】ZhuanLan非官方知乎专栏 - Android项目地址: https://gitcode.com/gh_mirrors/zh/ZhuanLan创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

告别Windows USB弹出烦恼:3个技巧让设备安全移除更简单

告别Windows USB弹出烦恼:3个技巧让设备安全移除更简单

告别Windows USB弹出烦恼:3个技巧让设备安全移除更简单 【免费下载链接】USB-Disk-Ejector A program that allows you to quickly remove drives in Windows. It can eject USB disks, Firewire disks and memory cards. It is a quick, flexible, portable altern…

2026/9/28 6:52:39 阅读更多 →
PoiyomiToonShader光照系统详解:从原理到实战的卡通渲染指南

PoiyomiToonShader光照系统详解:从原理到实战的卡通渲染指南

1. 项目概述:为什么PoiyomiToonShader值得你投入时间?如果你在Unity里做卡通渲染,尤其是二次元风格的项目,那么PoiyomiToonShader这个名字你大概率不会陌生。它早已不是一个小众工具,而是成为了大量独立开发者、学生乃…

2026/10/12 5:08:19 阅读更多 →
Blender材质合并与纹理图集生成:Material Combiner技术实现深度解析

Blender材质合并与纹理图集生成:Material Combiner技术实现深度解析

Blender材质合并与纹理图集生成:Material Combiner技术实现深度解析 【免费下载链接】material-combiner-addon Blender addon for material combining, uv bounds fixing 项目地址: https://gitcode.com/gh_mirrors/ma/material-combiner-addon Material Co…

2026/10/11 12:07:38 阅读更多 →

最新新闻

Flask轻量级环卫工人管理系统:排班考勤与部署实践

Flask轻量级环卫工人管理系统:排班考勤与部署实践

做环卫工人管理系统这个项目,最初源于一个很具体的需求:朋友所在的环卫站每天还要靠纸质表和微信群去排班、点名、对考勤,月底算工资更是头疼,经常因为漏记、重复记吵得不可开交。当时我正好手头熟悉 Python,就决定用 …

2026/10/12 9:15:51 阅读更多 →
Spring Boot植物识别养护平台开发:从数据建模到远程部署全流程

Spring Boot植物识别养护平台开发:从数据建模到远程部署全流程

1. 这个选题到底解决什么问题:学生项目里的"识别"和"养护"其实是两件事我做毕业设计辅导这几年,每年春秋两季都会收到一批"植物识别"相关的需求。早几年大家还在用百度识花之类的公共接口,后来很多人开始想自己…

2026/10/12 9:15:51 阅读更多 →
PS5新机设置全攻略:显示、存储、散热与串流优化指南

PS5新机设置全攻略:显示、存储、散热与串流优化指南

拿到PS5的第一周,相信很多人都会经历一段“机能明显很强,但用起来总差一口气”的时期。我前后帮A朋友、B朋友陆续整理过三台不同版本的主机,发现大家遇到问题的高度重合:存储空间转眼就满、风扇偶尔像飞机起飞、串流画质又糊又卡、…

2026/10/12 9:15:51 阅读更多 →
AnyPS5模拟器技术解析:运行时迁移与兼容层实现

AnyPS5模拟器技术解析:运行时迁移与兼容层实现

很多人第一次看到 AnyPS5 这个名字,第一反应多半是:又一个不靠谱的主机模拟器画饼。毕竟 PS4 模拟器折腾了这么多年都还没到人人可玩的程度,PS5 这种还在售的次世代主机,谈模拟听起来确实像天方夜谭。但真去翻一遍这块项目的代码和…

2026/10/12 9:15:51 阅读更多 →
mem0:为AI Agent构建可检索、可管控的外挂记忆系统

mem0:为AI Agent构建可检索、可管控的外挂记忆系统

1. 项目概述:为什么一个“外挂记忆”正在改变AI Agent的实战能力边界 最近在几个技术社区里,频繁看到开发者讨论“mem0”这个词,不是某个新出的模型,也不是某家大厂的闭源服务,而是一个开源的、专为AI Agent设计的记忆…

2026/10/12 9:15:51 阅读更多 →
TreeSet/TreeMap内容被覆盖?根因藏在可变对象和比较器里

TreeSet/TreeMap内容被覆盖?根因藏在可变对象和比较器里

做Java开发这些年,TreeSet和TreeMap我用得不算少。排序去重、排行榜维护、区间查询这些场景,它们都是非常趁手的工具。但我得坦白说,前阵子在一套内部排行榜模块里,我竟然被这两个容器搞得焦头烂额——业务方反馈榜单上的人变少了…

2026/10/12 9:14:51 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:53 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →