用Mox(Elixir)模拟嵌套函数

时间:2018-01-28 20:15:41

标签: mocking elixir

我是模拟和Elixir的新手,并且尝试使用模拟库Mox来改进我的测试覆盖率(模拟依赖项),我希望能够为大多数最关键的状态处理器创建测试,并且我的应用程序需要的其他计算。

所以我已经能够使用该库了,作为第一种方法,我得到了这个模拟函数来测试:

test "Mocked OK response test" do
    Parsers.MockMapsApi
    |> expect(:parse_get_distance_duration, fn _ -> {:ok, "test_body", 200} end)

    assert {:ok, "test_body", 200} == Parsers.MockMapsApi.parse_get_distance_duration({:ok, "", 200})
end

很明显,这个测试没用,因为我想要模拟的是另一个函数中的NESTED函数,但只是直接模拟测试中调用的函数。

所以,现在指出我的实际问题是一个更简单的测试,这很好地说明了这个场景:

def get_time_diff_to_point(point_time) do
            point_time
            |> DateConverter.from_timestamp()
            |> Timex.diff(Timex.now(), :seconds)
            |> Result.success()
    end  

显然,Timex.now()每次都会给我一个新的时间戳,因此无法在没有模拟的情况下成功测试计算。所以真正的问题是,如何在被测试的实际函数中模拟NESTED函数?在这种情况下,我希望每次运行测试时Timex.now()给我相同的价值......这是我到目前为止所得到的(显然不起作用,但我认为说明了我'我试图这样做:

test "Mocked time difference test" do
    Utils.MockTimeDistance
    |> expect(:Timex.now(), fn -> #DateTime<2018-01-28 20:13:43.137007Z> end)

    assert {:ok, 4217787010} == Utils.TimeDistance.get_time_diff_to_point(5734957348)
end

1 个答案:

答案 0 :(得分:1)

Mox方法明确区分了这一点:您当前的代码对Timex.now具有非常特定的依赖性,如果没有它,则无法编译。

但是,在您看来,您的代码只依赖于能够找出当前时间,而且您希望在测试期间模拟这种依赖关系。

这就是Mox坚持只模拟行为的原因:你被迫专门识别必须控制的外部依赖。

所以,要采用Mox方式,你需要做的是:

  1. 找到提供行为的时间库,-or-
  2. Timex.now包装在您自己的模块中,该模块实现了Mox可以使用的行为。
  3. 例如,您可以编写如下内容:

    defmodule TimeProvider do
      @callback now() :: DateTime.t
    
      @behaviour __MODULE__
      @impl __MODULE__
      def now(), do: Timex.now()
    end
    

    现在,您需要使用依赖项注入策略来调用Timex.now的实现,而不是从get_time_diff...方法调用TimeProvider。执行依赖注入的两种方法:使用Application.get_env(app, key)从config中检索模块(在config.exs中将其设置为TimeProvider,但在测试中将其覆盖为MockTimeProvider),或将模块设置为默认值仅在测试中被覆盖的参数。

    def get_time_diff_to_point(point_time, time_provider \\ TimeProvider) do
                point_time
                |> DateConverter.from_timestamp()
                |> Timex.diff(time_provider.now(), :seconds)
                |> Result.success()
    end  
    

    Mox需要以下设置:test_helper.exs中的某个地方:

    Mox.defmock(MockTime, for: TimeProvider)
    

    并在你的测试中:

    test "time_diff" do
      expect(MockTime, :now, fn -> DateTime.from_unix!(0) end)
      assert get_time_diff_to_point(DateTime.from_unix!(1), MockTimeProvider) == ...
    end
    

    请注意,一般来说,模拟时间既困难又重要。这是一个简单的例子,你知道被测代码中的其他函数调用都不依赖于时间,now()只会被调用一次:你可以将它视为正常的模拟:但要注意更多涉及/时间感知代码的测试时间。

    修改

    经过一番思考之后,你可以通过添加单层抽象来干掉你的代码并只在一个地方进行依赖注入:

    defmodule MyApp.Time do
      def now() do
        time_provider = Application.get_env(:my_app, :time_provider)
        time_provider.now()
      end
      defmodule TimeProvider do
        @callback now() :: DateTime.t
      end
      defmodule DefaultTimeProvider do
        @behaviour TimeProvider
        @impl TimeProvider
        def now(), do: Timex.now()
      end
    end
    

    在你的config.exs中,你需要:

    config :myapp, time_provider: MyApp.Time.DefaultTimeProvider
    

    在您的测试设置中,您需要:

    Mox.defmock(MockTime, for: TimeProvider)
    Application.put_env(:my_app, :time_provider, MockTime)
    

    通过删除可选参数,简化了您的示例(并且,重要的是,所有其他用法):

    def get_time_diff_to_point(point_time) do
      point_time
      |> DateConverter.from_timestamp()
      |> Timex.diff(MyApp.Time.now(), :seconds)
      |> Result.success()
    end
    

    这两种方法都有优点 - 两者兼有,并了解哪种情况最适合每种情况。