ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

面向接口+单元测试:大厂工程师怎么写代码,30个测试怎么规划,别名bug全拆解

面向接口+单元测试:大厂工程师怎么写代码,30个测试怎么规划,别名bug全拆解 【Java实战】面向接口 单元测试大厂工程师怎么写代码30个测试怎么规划那个别名 bug到底怎么回事系列《研发效能智能助手》实战专栏 · 第 3 篇上篇回顾《实体设计与JSON持久化手写一个不丢数据的数据层原子落盘/损坏兜底/ID种子全拆解》 点这里看第 2 篇配套源码完整可运行 Maven 工程JDK 17 30 个单元测试全绿文末附下载一、先说一个每天都在发生的改代码恐惧症你有没有过这种感觉一个功能写好了领导说加个字段你心里一紧——改完这行会不会把别的地方弄崩我第一版代码片段管理器就是这样UI 层直接 new 了一个FileSnippetRepository数据操作散得到处都是。某天我想把存储从文件换成数据库打开代码一看——七八个地方都在直接读写文件改一个崩一片。更可怕的是改了以后你不知道崩没崩。程序跑起来看着正常直到某个用户操作触发那条没测过的路径数据就没了。这一篇就讲两件事是把我从这种恐惧里捞出来的两个武器面向接口设计让换底层实现变成改一行配置的事单元测试让改完不知道崩没崩变成跑一遍30 秒内见分晓。二、为什么先写接口不先写实现这是依赖倒置很多新手写代码的顺序是先写一个类把功能塞进去谁要用直接 new。大厂的习惯正好反过来——先定义我要什么能力再写怎么实现。这个我要什么能力就是接口。看项目里的仓储接口总共五个方法每个都带契约注释publicinterfaceSnippetRepository{/** 查询全部代码片段空库返回空列表永不返回 null */ListCodeSnippetfindAll();/** 按 ID 查询未命中返回 Optional.empty() */OptionalCodeSnippetfindById(Longid);/** 保存代码片段新增与更新统一入口 */voidsave(CodeSnippetsnippet);/** 按 ID 删除返回是否删除成功 */booleandeleteById(Longid);/** 获取数据文件的完整路径 */StringdataFilePath();}为什么业务层只依赖这个接口因为业务层根本不需要知道数据存在文件里还是数据库里。它只关心保存、查询、删除这些能力。那好处是什么看这个场景——以后项目升级存储换 MySQL// 现在的实现文件存储SnippetRepositoryrepositorynewFileSnippetRepository(path);// 将来的实现数据库存储业务层代码零改动SnippetRepositoryrepositorynewJdbcSnippetRepository(dataSource);换实现 改一行。SnippetService里所有代码一行不动因为它是面向接口写的。这就是面试官爱问的依赖倒置原则DIP高层模块业务不依赖低层模块文件/数据库大家都依赖抽象接口。翻译成人话业务规则是稳定的存储方式是易变的把变化关进小笼子。三、接口怎么设计才算大厂水平不是随便抽个接口就完事了设计里有几个讲究全是面试考点3.1 空值语义能返回空集合就绝不返回 null/** 查询全部代码片段空库返回空列表永不返回 null */ListCodeSnippetfindAll();为什么null 是所有空指针异常的源头。调用方拿到 null忘了判空一调用就崩。返回空列表调用方随便 for 循环安全。这就是为什么大厂代码规范里经常写方法返回值禁止裸 null。3.2 用 Optional 表达可能没有OptionalCodeSnippetfindById(Longid);ID 不存在怎么办以前的做法是返回 null调用方if (obj ! null)写一堆。用Optional把可能没有这个事实写进方法签名调用方必须处理repository.findById(id).orElseThrow(()-newSnippetException(未找到 ID 为 id 的片段。));没找到就抛业务异常调用方想忘都忘不了。3.3 新增和更新统一成一个方法/** 保存代码片段新增与更新统一入口 */voidsave(CodeSnippetsnippet);很多新手会写add()和update()两个方法。但仔细想想新增和更新在数据层面就是一件事按 ID 覆盖——ID 不存在就是新增ID 存在就是更新。统一成save()接口少一半语义还更清楚。3.4 方法名和返回值要自解释deleteById返回boolean——删没删掉一眼就知道。而不是返回void让调用方去猜到底删没删成功。接口是给别人用的注释把契约写清楚返回值把结果表达清楚。这就是好的接口设计。四、30 个测试怎么规划按层拆按规则建测试不是越多越好是每一条业务规则都有一条测试守护。这个项目 30 个测试拆成两层测试类层覆盖什么数量FileSnippetRepositoryTest仓储层持久化、查询、删除、损坏兜底、ID种子9SnippetServiceTest业务层校验、搜索、筛选、编辑、收藏、分页、导出21合计 30 个全绿怎么规划测试对着业务规则清单一条条来。项目开发文档里写了 8 条业务规则测试就照着规则建// 业务规则清单文档里写的// 1. 标题必填且不超过 50 字符// 2. 代码内容必填// 3. 标签去空白、去重最多 5 个// 4. 搜索是标题/内容/标签模糊匹配忽略大小写// 5. 分页页码从 1 开始超页返回空页// 6. 收藏切换不刷新更新时间// ...// 每条规则至少一个成功路径测试 一个失败路径测试TestDisplayName(新增成功返回带 ID/时间字段的片段收藏默认为 false)voidaddSnippet_success(){...}TestDisplayName(新增失败标题为空抛出业务异常)voidaddSnippet_blankTitle_throws(){...}规则是需求测试是规则的执行证明。开发文档里的规则清单就是测试清单的来源——这就是测试驱动最朴素的落地方式不是先写代码再补测试而是先有规则测试和实现同时生长。三个测试编写规范直接背下来方法名用行为描述addSnippet_blankTitle_throws——一眼看懂测什么场景、期望什么结果用DisplayName写中文测试报告给谁看给团队看中文描述让每个人包括以后的你秒懂每个测试独立隔离用TempDir临时目录测试之间互不污染随便重复跑。TempDirPathtempDir;// JUnit 自动创建、自动清理的临时目录BeforeEachvoidsetUp(){// 每个测试都用一个全新仓储绝不复用上一个测试的脏数据FileSnippetRepositoryrepositorynewFileSnippetRepository(tempDir);servicenewSnippetService(repository);}五、重点来了那个别名 bug到底是怎么回事第 2 篇文末预告留了个钩子现在揭晓。写完updateSnippet编辑功能后我写了这个测试TestDisplayName(编辑字段更新、更新时间刷新、创建时间保留)voidupdateSnippet(){CodeSnippetoriginalservice.addSnippet(旧标题,Language.JAVA,List.of(旧标签),old code);LocalDateTimecreatedAtoriginal.getCreatedAt();LocalDateTimeupdatedAtBeforeoriginal.getUpdatedAt();CodeSnippetupdatedservice.updateSnippet(original.getId(),新标题,Language.PYTHON,List.of(新标签),new code);assertEquals(新标题,updated.getTitle());assertEquals(createdAt,updated.getCreatedAt(),创建时间必须保留);assertNotEquals(updatedAtBefore,updated.getUpdatedAt(),更新时间必须刷新);}跑起来红了。报错的是最后一行更新时间必须刷新断言失败。我盯着代码看了十分钟updateSnippet里明明执行了existing.setUpdatedAt(LocalDateTime.now())时间怎么会没变然后我想明白了——这就是对象别名aliasing// service.updateSnippet 内部做的事CodeSnippetexistingrepository.findById(id).get();// 从仓库取出对象existing.setUpdatedAt(LocalDateTime.now());// 原地修改repository.save(existing);// 又存回去仓储里存的和外面拿到的是同一个对象引用。我的测试里updatedAtBefore也是从这个对象上读的——updateSnippet 改了它updatedAtBefore也跟着变了于是旧值和新值永远相等断言必挂。这不是测试写错了是测试揪出了一个真实的代码隐患对象被原地修改外部无感知。怎么修两个层面第一层测试层面快照旧值要在修改之前拿而且知道别名现象后测试本身就是一份文档// 注意仓储保存的是对象引用更新是原地修改别名现象// 因此必须在更新前先快照旧值再与更新后的值比较。LocalDateTimecreatedAtoriginal.getCreatedAt();LocalDateTimeupdatedAtBeforeoriginal.getUpdatedAt();第二层设计层面真正的根治是让实体不可变或防修改——第 2 篇讲的防御性拷贝就是干这个的setTags拷贝进来、getTags只读视图返回。别名问题不是 bug 是特性但特性会咬人测试就是那个提前咬醒你的。这个故事的教训测试不是形式主义它是代码的显微镜。肉眼看不出的引用问题一个断言 3 秒就照出来了。六、测试怎么证明程序不丢数据第 2 篇讲了原子落盘、损坏兜底、ID 种子延续——这些设计不是靠嘴说的是靠测试证明的。看仓储层两个最硬核的用例用例 1数据文件损坏程序不能崩TestDisplayName(数据文件损坏自动备份并重建空库程序不崩溃)voidload_corruptedFile_recovers()throwsIOException{// 1. 先正常使用并落盘repo.save(snippet(1L,正常数据));PathdataFilePath.of(repo.dataFilePath());// 2. 人为破坏数据文件写入非法 JSONFiles.writeString(dataFile,{ 这不是合法的 JSON,StandardCharsets.UTF_8);// 3. 重新构造仓储 模拟程序重启不应抛异常应自动兜底FileSnippetRepositoryrepo2newFileSnippetRepository(tempDir);assertTrue(repo2.findAll().isEmpty(),损坏恢复后应为空库);// 4. 检查损坏文件已被自动备份snippets.json.bak-时间戳try(varfilesFiles.list(dataFile.getParent())){booleanhasBackupfiles.anyMatch(p-p.getFileName().toString().startsWith(snippets.json.bak-));assertTrue(hasBackup,损坏文件应被自动备份);}}这个测试 5 行代码把第 2 篇讲的损坏兜底完整证明了一遍写坏文件 → 重启 → 不崩溃 → 自动备份 → 重建空库。以后谁改坏了兜底逻辑测试第一个报警。用例 2重启后 ID 不冲突TestDisplayName(重启后 ID 种子延续新 ID 不与历史冲突)voididSeed_continuesAfterReload(){FileSnippetRepositoryrepo1newFileSnippetRepository(tempDir);repo1.save(snippet(1L,旧数据));// 模拟重启同一目录重新构造仓储FileSnippetRepositoryrepo2newFileSnippetRepository(tempDir);repo2.save(snippet(2L,新数据));ListCodeSnippetallrepo2.findAll();assertEquals(2,all.size(),新旧数据应共存);// 两条数据 ID 各不相同证明新 ID 没有从 1 重新开始assertEquals(2,all.stream().map(CodeSnippet::getId).distinct().count(),ID 不允许冲突);}测试就是活的文档。别人看不懂你的设计看测试。测试把“系统应该怎么表现”用代码写死了比任何注释都准确。七、这套打法能迁移到任何语言你以为这只是 Java 的事错了。面向接口 单元测试是跨语言的通用工程素养。后面这个系列项目会进 PythonJava Python 智能体平台。Python 里这套打法完全成立# Python 版同一个依赖倒置思想classSnippetRepository(ABC):abstractmethoddeffind_all(self)-list:...# Python 版同一个测试守护deftest_update_snippet_refreshes_updated_at():# pytest 的 TempDir 等价物...接口的语法不同但思想完全一样上层依赖抽象测试守护规则。这就是为什么大厂招人爱问设计原则和测试——因为换语言不换思维招的是能迁移的人。面试可以直接说我写过 30 个单元测试覆盖仓储层和业务层全部关键路径我踩过对象别名的坑知道防御性拷贝为什么存在我知道测试不是形式主义是让改代码不怕崩的底气。八、源码在哪拿完整源码 开发文档 30 个单元测试全绿 运行演示截图都在下面这个包里 Java代码片段管理器-完整源码开发文档下载包里包含完整 Maven 工程源码UI / Service / Repository / 存储 四层面向接口设计-30 个 JUnit 单元测试全绿仓储 9 业务 21含损坏兜底、ID 种子、别名场景用例开发文档含业务规则清单——测试清单的唯一来源README 使用说明 运行演示截图适合谁看完第 2 篇想搞懂测试到底怎么写才有价值的人面试被问依赖倒置/单元测试只能背概念、说不出实战的人想抄一套测试规范直接用到自己项目里的人。九、下期预告第 4 篇《完整源码 面试考点总结》——把前三篇的设计点全部转成面试题逐个击破依赖倒置、防御性拷贝、原子落盘、损坏兜底、对象别名……一个不落本系列共 9 个项目从命令行工具一路进阶到Java Python 智能体完整平台。关注专栏跟着我一步步从零实战到就业。如果这篇对你有帮助点赞 收藏 关注是我持续更新的最大动力。有任何问题欢迎评论区留言我看到都会回复。
返回列表