有没有办法在编译时向elixir模块添加函数?

时间:2017-07-10 21:50:00

标签: elixir

我正在使用长生不老药并遵循建议 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

我想我遇到了编译问题的顺序。

有没有办法在编译时向模块添加函数?

3 个答案:

答案 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)

在这个例子中,上述模块的行为将是......

  1. YourApp.Module.Sandbox - Hit是您正在与之交互的任何API的开发沙箱,如果有的话。在开发过程中简单地使用YourApp.Module.InMemory并不罕见。只取决于你使用它的目的。
  2. YourApp.Module.InMemory - 此模块中的所有API交互都只返回静态内联数据。例如表示真实API将发回的结构列表
  3. YourApp.Module.RealHTTP - 真正的HTTP互动。
  4. 正如文章还指出的那样,上述每个模块都会实现相同的行为(即通过@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”,但我需要它们以便编译顺序正确,而且我认为无论如何都无法将代码注入其他模块。