【问题标题】:Is there a way to add a function to an elixir module at compile time?有没有办法在编译时向 elixir 模块添加函数?
【发布时间】:2023-03-15 05:10:02
【问题描述】:

我正在使用 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

我想我遇到了编译问题。

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

【问题讨论】:

    标签: elixir


    【解决方案1】:

    您是否尝试过use 宏?你可以阅读更多关于它的信息here。如果我正确理解了您的问题,您似乎需要使用 &amp;__using__/1 回调将功能注入您的模块。

    编辑: 我只是不确定有没有办法在编译时在没有宏的情况下向模块添加函数......我们使用以下宏:

    defmacro define(name, value) do
      quote do
        def unquote(name), do: unquote(value)
      end
    end
    

    要定义常量,也许您可​​以收到block 而不是value

    【讨论】:

    • 我已经在我编写的其他代码中使用了 use。我可能最终会重构我的第一次尝试并添加 use 关键字来稍微清理我的第一次尝试。我想知道是否有一种更优雅的方法可以直接向模块添加函数,而无需在我的第一种方法中使用钩子。
    • Yw。在与 cmets 战斗后,我决定编辑我的答案更容易。
    • 诀窍是我想使用宏在另一个模块上定义一个函数,而不是调用宏的位置。
    【解决方案2】:

    我能够弄清楚以下几点:

    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
    

    我最初试图避免调用“使用”,但我需要它们以便编译顺序正确,而且我认为无论如何都无法将代码注入其他模块。

    【讨论】:

      【解决方案3】:

      您能否提供更多关于“我遇到了跟​​踪哪个模拟函数对应于哪个测试的问题”的更多信息,因为我认为您可能过于复杂了。

      根据您链接的文章,您将使用 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 交互。

      正如文章还指出的,上述每个模块都将实现相同的行为(即,通过 @behaviour 实现的 Elixir 行为,这确保每个模块都实现了必要的功能,因此您知道您的 InMemory 模块将像可靠地作为您的 RealHTTP 模块。

      我意识到我几乎只是重复了一些文章,但除此之外,我并没有真正理解您的问题。

      【讨论】:

      • 这就是我正在做的。当我在 InMemory 模块中编写函数时,我有足够的时间来跟踪哪些函数对应于哪些测试。我使用模式匹配来为同一个函数调用获取不同的返回值。后来我意识到我可以使用 inmemory 模块中的 cmets 来跟踪。我刚开始想知道是否有一种方法可以实现与相同的 ruby​​ 模拟类似的语法。
      • 对不起,我还是不明白..你能发布一个你的 InMemory 模块的例子,这样我就可以看到是什么让事情变得足够复杂以至于“迷失方向”,以及你围绕它的测试,太棒了。
      • 作为工作代码,我不能分享。我们正在模拟一个 http 库,我们的应用程序向多个第三方服务发出大量请求,因此我们最终得到了 100 行模拟响应
      • 我们对同一个服务有很多不同的响应,因此我们无法真正将其分解为不同的模块
      猜你喜欢
      • 1970-01-01
      • 2011-12-22
      • 1970-01-01
      • 1970-01-01
      • 2012-12-26
      • 1970-01-01
      • 2015-04-24
      • 2015-04-08
      • 2018-08-16
      相关资源
      最近更新 更多