单元测试需 mock 全局状态或单例,因业务模块硬编码依赖(如 new DatabaseClient)导致无法隔离;DI 的本质是将依赖创建权移出模块,由外部注入所需接口而非具体实现。

为什么单元测试总要 mock 全局状态或单例?
因为业务模块直接 new 了数据库连接、HTTP 客户端或配置管理器,导致测试时无法隔离外部依赖。DI 的核心不是“用框架”,而是把依赖的创建权从模块内部移出去——让模块只声明“我需要一个 UserService”,而不关心它怎么来。
实操建议:
- 识别所有硬编码依赖:搜索
new DatabaseClient()、Config.getInstance()、HttpClient.create()这类调用,它们是 DI 的首要改造目标 - 优先对“有副作用”或“难模拟”的类型引入接口抽象,比如把
FileStorage抽成StorageService接口,而非直接注入具体类 - 避免在构造函数里做实际工作(如连接池初始化),否则 DI 容器实例化时就会失败,应延迟到首次调用或显式
init()
Spring Boot 中 @Autowired 不生效?检查这三处
常见现象是字段为 null,或测试中注入的是真实实现而非 mock——根本原因常不在注解本身,而在 Bean 生命周期和作用域错位。
实操建议:
- 确认类被 Spring 管理:必须是
@Service、@Component或显式@Bean注册,手写new MyService()会绕过 DI 容器 - 测试类需启用上下文:JUnit 5 下用
@ExtendWith(SpringExtension.class)+@ContextConfiguration,或更简单的@SpringBootTest(但注意启动开销) - Mock 替换要精准:用
@MockBean而非@Mock,后者只是普通 mock,不会替换容器中的 Bean;若需部分 mock,用@SpyBean并小心循环代理问题
Go 项目没 IOC 容器,怎么手动实现轻量 DI?
Go 没反射驱动的自动装配,但正因如此,DI 更清晰可控——关键在于把依赖组装逻辑集中到 main() 或 cmd/ 层,而不是散落在各 package 的 init 函数里。
实操建议:
- 定义依赖图入口函数,例如
NewApp(config Config) *App,内部按顺序创建DB→Repo→Service→Handler,并逐层传入 - 接口即契约:为 HTTP 客户端定义
HTTPClient interface { Do(req *http.Request) (*http.Response, error) },测试时直接传入闭包实现 - 避免全局变量污染:不要用
var db *sql.DB,而是在main()创建后,作为参数注入到需要它的结构体字段中
测试中 DI 导致“Too many dependencies”怎么办?
当一个 handler 构造函数要传 7 个参数,说明职责过重,或依赖粒度太细。DI 是手段,不是目的——可测试性差的根源常是模块边界模糊。
实操建议:
- 合并高内聚依赖:把
UserRepo、RoleRepo、PermissionRepo封装进AuthRepository接口,减少构造参数数量 - 用组合替代堆砌:不注入
Logger+Metrics+Tracer,而是注入Instrumentation结构体,内部聚合它们 - 警惕“伪 DI”:只为测试而加接口,但实现类永远只有一个,且接口无真实多态价值——这类抽象反而增加维护成本
真正难的不是写 @Inject 或传参,而是判断哪些依赖该被抽象、哪些该保持具体。边界划错,DI 就变成套娃式复杂度。

















