Elixir中如何mock模块属性与外部模块?实现方式与最佳实践
先明确核心特性
Elixir的模块属性是编译期求值的:你在defmodule B里写的@my_module_attribute A.my_first_function(),会在编译B模块的阶段直接执行A.my_first_function(),把返回值固化到B模块的元数据里,不会留到运行时再去调用A的方法。这是理解后续所有方案的前提,也是很多初学者容易踩的坑。
如何Mock模块A
Elixir社区不推荐运行时动态替换模块实现的打补丁式mock,容易造成全局状态污染、测试结果不稳定,优先用依赖注入的方式实现可替换的依赖:
- 第一步:不要在B模块里硬编码依赖A,把依赖做成可配置项
defmodule B do # 编译时从应用配置取依赖模块,生产环境默认用A,测试环境可替换 @module_a Application.compile_env(:your_app_name, :module_a, A) @my_module_attribute @module_a.my_first_function() # 如果是运行时需要调用A的方法,统一通过@module_a调用 def some_runtime_logic do @module_a.my_first_function() end end
- 第二步:在测试环境的配置文件(config/test.exs)里指定mock实现
config :your_app_name, :module_a, MockedA
- 第三步:测试文件里定义Mock模块,自定义返回值
defmodule MockedA do def my_first_function do # 这里写你需要的自定义测试返回值即可 :test_expected_value end end
如果是仅在运行时调用的方法,还可以用更轻量的参数注入方式,连配置都不用改:
defmodule B do # 默认依赖A,测试时可以手动传入mock模块 def some_logic(dep_mod \\ A) do dep_mod.my_first_function() end end
测试的时候直接传入自定义的mock模块作为参数即可,完全隔离无副作用。
如果你维护的是老代码,已经硬编码写死了对A的依赖、暂时没法重构,也可以用支持动态模块补丁的库临时替换A的方法实现,但要注意:这种方式对编译期求值的模块属性完全无效——因为模块属性的值在编译阶段就已经计算固化了,测试运行时再修改A的方法逻辑已经晚了。另外这类方案会修改全局代码状态,一定要记得在测试结束后还原原实现,否则会污染其他测试用例。
如何处理@my_module_attribute模块属性的Mock
已经编译完成的模块属性没有办法直接mock,它本质是存在模块上的固定常量,不是可动态替换的逻辑。要实现测试时自定义该值的效果,只有两种合规方案:
- 用前面提到的编译期依赖注入方案,在测试环境编译阶段就把依赖的A模块替换为mock模块,这样编译时计算出来的
@my_module_attribute自然就是mock方法返回的自定义值,不需要额外修改属性本身的逻辑。 - 如果需要更灵活的运行时替换能力,不要把这类需要动态变更的值存在模块属性里,封装成私有函数在运行时求值即可:
defmodule B do defp my_module_attribute do A.my_first_function() end end
这种写法下值是运行时调用函数时才计算的,不管是依赖注入还是动态mock都能正常生效。
最佳实践总结
- 所有跨模块依赖优先用显式依赖注入实现,不要在代码里硬编码依赖的模块名,编译期固定的配置用
Application.compile_env注入,运行时动态的逻辑用默认参数注入,这是Elixir社区公认的可测试性最佳实践,不需要奇技淫巧就能完成测试mock,也没有全局副作用。 - 模块属性仅用来存编译期可确定、所有环境下值都一致的常量,凡是需要按环境替换、测试时需要mock的值,不要直接在模块顶层赋值给模块属性。
- 尽量避免用运行时动态打模块补丁的mock方案,这类方案测试通过不代表生产逻辑正确,很容易引入隐蔽的测试偶发失败问题。
内容的提问来源于stack exchange,提问作者Red Baron
相关产品推荐
相关产品推荐

