Return 不同的值取决于调用函数的次数

Return different values depending on number of times a function is called

背景

我在一个使用 Mock 进行测试的项目中工作。但是我需要 运行 一个特定的场景,其中函数的输出取决于调用该函数的次数。 我不认为 Mock 支持这个,所以我想找到一种方法来进行这个测试。

代码

在这个测试中,我有一个我想模拟的存储模块(它有副作用并且是一个边界)。 在测试中,我调用了一个函数 get,第一次是 returns nil,然后我用 save 保存了一些数据,然后我再次调用了 get。

test_with_mock "returns OK when products list is saved into storage", Storage, [],
    [
      save: fn _table, data -> {:ok, data} end,
      get: fn
        _table, _seats_number -> {:ok, nil} # first call it returns nil
        _table, _seats_number -> {:ok, [1]} # second call should return some data
      end
    ] do
      # Arrange
      products = [
        %{"id" => 1, "gems" => 4},
        %{"id" => 3, "gems" => 4},
      ]
      products_table = :products

      # Act & Assert
      actual = Engine.save_products(products)
      expected = {:ok, :list_saved_successfully}

      assert actual == expected
      
      # First call to get returns nil because the table is empty. 
      # Then we save something into it.
      assert_called Storage.get(products_table, 4)
      assert_called Storage.save(products_table, {4, [1]})

      # Second call should return the product previously saved
      # But the mock only returns nil
      assert_called Storage.get(products_table, 4)
    end

这里的问题是,由于没有计数器,我无法根据调用函数的次数返回不同的输出。

公平地说,当输入不同时,Mock确实提供了一种returns不同输出的方法。然而,这种情况并非如此。输入是一样的,唯一不同的是调用次数。

问题

如何使用 Mock 实现我的目标?

您可以使用函数 :meck.seq 创建一个 return 按顺序给定值的模拟。这依赖于 Mock 只是 meck 之上的薄层这一事实,它允许创建这样的模拟:

  test_with_mock "returns OK when products list is saved into storage", Storage, [],
    [
      save: fn _table, data -> {:ok, data} end,
      get: [{[:_, :_], :meck.seq([
               {:ok, nil}, # first call it returns nil
               {:ok, [1]} # second call should return some data
               ])}]
    ] do

也就是说,不是传递一个匿名函数作为模拟的实现,而是传递一个元组列表,其中每个元组都有一个“参数规范”(在本例中 [:_, :_],允许任意两个arguments) 和一个“return spec”,其中 :meck.seq return 是一个神奇的值,它使模拟 return 每次都有不同的值。

您可以使用 agent 来保持可变状态。这是草图:

defmodule MagicTest do
  use ExUnit.Case, async: false

  import Mock

  setup do
    {:ok, pid} = Agent.start_link(fn -> 1 end, name: __MODULE__)
    {:ok, %{counter: pid}}
  end

  test_with_mock "test_name", %{counter: counter}, Magic, [],
    get: fn -> Agent.get_and_update(counter, fn state -> {state, state + 1} end) end do
    assert 1 == Magic.get()
    assert 2 == Magic.get()
  end
end


defmodule Magic do
  def get do
    42
  end
end

您的测试可能会更复杂一些,但思路是一样的。

模拟自述文件还 briefly explains 如何使用 ExUnit 上下文参数。