我正在使用长生不老药并遵循建议 http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/ 博客文章。
我遇到了跟踪哪个模拟函数对应的问题 哪个测试。我为api包装器的测试环境添加了一个模拟模块。当我向mock api模块添加模拟函数时,我发现我不记得编写了哪些函数来返回哪些测试的结果。
我一直试图找出一种方法来使用宏来定义测试附近的模拟方法。我也对这个问题感兴趣,作为一种学习练习。
以下是我设想的方法:
defmodule SomeMockModule do
end
defmodule MockUtil do
defmacro add_mock module, block do
# <THE MISSING PIECE>
end
end
defmodule Test do
use ExUnit.Case
require MockUtil
MockUtil.add_mock SomeMockModule do
def some_func do
"mock value"
end
end
test "The mock value is returned" do
assert SomeMockModule.some_func == "mock value"
end
end
这与开放模块的问题类似: Open modules in Elixir? 但是我想知道如何在编译时而不是运行时这样做。
我环顾四周,并没有找到任何说它能够或不能在编译时完成的事情。
在某种程度上,这是一个花哨的复制和粘贴:)
到目前为止我尝试过:
1)以下作品但似乎相当混乱。它需要更改模拟模块。我试图找出是否有办法在没有编译之前完成它。
defmodule MockUtil do
defmacro register_function( _module, do: block )do
Module.put_attribute Test, :func_attr, block
end
end
defmodule Test do
require MockUtil
Module.register_attribute __MODULE__,
:func_attr,
accumulate: true, persist: false
defmacro define_functions(_env) do
@func_attr
end
MockUtil.register_function SomeMockModule do
def foo_bar do
IO.puts "Inside foo_bar."
end
end
end
defmodule SomeMockModule do
@before_compile {Test, :define_functions}
end
SomeMockModule.foo_bar
2)我也试过,代替:
Module.eval_quoted module, block
然而它会抛出错误:
could not call eval_quoted on module {:__aliases__, [counter: 0, line: 10], [:SomeMockModule]} because it was already compiled
我想我遇到了编译问题的顺序。
有没有办法在编译时向模块添加函数?
答案 0 :(得分:1)
您是否尝试过use
宏?您可以阅读更多相关信息here。如果我正确地理解了您的问题,那么在您的模块中注入功能似乎就是您需要的&__using__/1
回调。
编辑: 我只是不确定有没有办法在没有宏的情况下在编译时向模块添加函数...我们使用以下宏:
defmacro define(name, value) do
quote do
def unquote(name), do: unquote(value)
end
end
要定义常量,可能代替value
而不是block
?
答案 1 :(得分:0)
您是否能够提供更多有关“我遇到跟踪哪个模拟功能与哪个测试相对应的问题”的信息,因为我认为您可能过于复杂了。
根据您链接的文章,您将使用OTP应用程序配置来指定在哪个环境中使用哪个模块。在prod
中,您需要使用“真正的”HTTP客户端,例如。
# config/dev.exs
config :your_app, :module_to_mock, YourApp.Module.Sandbox
# config/test.exs
config :your_app, :module_to_mock, YourApp.Module.InMemory
# config/prod.exs
config :your_app, :module_to_mock, YourApp.Module.RealHTTP
然后,当您想要使用该模块时,您只需使用
抓取它Application.get_env(:your_app, :module_to_mock)
在这个例子中,上述模块的行为将是......
YourApp.Module.Sandbox
- Hit是您正在与之交互的任何API的开发沙箱,如果有的话。在开发过程中简单地使用YourApp.Module.InMemory
并不罕见。只取决于你使用它的目的。YourApp.Module.InMemory
- 此模块中的所有API交互都只返回静态内联数据。例如表示真实API将发回的结构列表YourApp.Module.RealHTTP
- 真正的HTTP互动。正如文章还指出的那样,上述每个模块都会实现相同的行为(即通过@behaviour
的Elixir行为,这可以确保每个模块实现必要的功能,因此您知道您的InMemory
模块将与RealHTTP
模块一样可靠地工作。
我意识到我几乎只是回避了一些文章,但除此之外,我并没有真正了解你的问题。
答案 2 :(得分:0)
我能够弄清楚以下内容:
ExUnit.start
defmodule MockUtil do
defmacro __using__(_opts) do
quote do
defmacro __using__(_env) do
test_module = __MODULE__
mock_module = __CALLER__.module
|> Atom.to_string
|> String.downcase
|> String.split(".")
|> tl
name = "#{mock_module}_functions_attr" |> String.to_atom
quote do
unquote(test_module).unquote(name)()
end
end
end
end
defmacro add_mock_function( module, do: block ) do
mock_module = Macro.expand_once( module, __CALLER__)
|> Atom.to_string
|> String.downcase
|> String.split(".")
|> tl
test_module = __CALLER__.module
functions_attribute = "#{mock_module}_functions_attr" |> String.downcase |> String.to_atom
first_time? = Module.get_attribute test_module, functions_attribute
Module.register_attribute test_module,
functions_attribute,
accumulate: true, persist: false
Module.put_attribute test_module, functions_attribute, block
if first_time? == nil do
ast = {:@, [], [{functions_attribute, [], test_module}]}
name = "#{mock_module}_functions_attr" |> String.to_atom
quote do
defmacro unquote(name)(), do: unquote(ast)
end
end
end
end
defmodule Test do
use ExUnit.Case
use MockUtil
MockUtil.add_mock_function Mock do
def foo do
"Inside foo."
end
end
test "Register function adds foo function" do
assert "Inside foo." == Mock.foo
end
MockUtil.add_mock_function Mock do
def bar do
"Inside bar."
end
end
test "Register function adds bar function" do
assert "Inside bar." == Mock.bar
end
MockUtil.add_mock_function MockAgain do
def baz do
"Inside bar."
end
end
test "Register function adds baz function" do
assert "Inside bar." == MockAgain.baz
end
end
defmodule Mock do
use Test
end
defmodule MockAgain do
use Test
end
我最初试图避免调用“use”,但我需要它们以便编译顺序正确,而且我认为无论如何都无法将代码注入其他模块。