jjh42 / mock

Mocking library for Elixir language

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

Fl4m3Ph03n1x opened this issue · comments

Background

I am working in a project that uses this library for testing. However I need to run a specific scenario, where the output of a function depends on the number of times said function is called.
I have read the documentation and I don't think it is possible to do this with Mock, but perhaps I am missing something.

Code

In this test I have a Storage module which I want to mock (it has side effects and is a boundary).
In the test, I call a function get, which the first time returns nil, then I save some data with save and then I call get again.

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

The issue here is that since there is no counter, I don't have a way of returning a different output depending on the number of calls a function was called.

To be fair, Mock does offer a way to returns different outputs when the input is different. However, this is not the case. The input is the same, the only thing different is the number of invocations.

I am aware of call history and meck, but as far as I understand, this is used more for asserts and not used to return specific outputs (as in, this is a history stack).

Question

How can I achieve my goal using Mock? (is it possible?)

Thanks for the detailed question!

Unfortunately, what you need to do is not currently possible with Mock and I don't have good ideas of how to achieve it.

It seems like you have a stateful function which kind of breaks the functional paradigm of elixir. With that being said, I do recognize that there are always exceptions and code is never perfect.

If you have an idea of how to implement this, PRs are welcome!

Hello @Olshansk . Everyone needs to manage state somewhere in their applications. If an application does not interact with the outside world or doesn't manage state whatsoever ... well, I find hard to imagine such an application :D

In this specific case, the Storage module is responsible for making CRUD operations in ETS tables. I use Mock to simulate these calls so I don't actually have to spin up an ETS table, populate, etc.

While I try to follow the functional paradigm as much as I can, I admit I have my own limits. If you know any patterns or recommend any books that you think would make this post useless, feel free to let me know.

As for ways of doing this, I am aware of at least 2:

  1. use the process's dictionary. Since mock is global, this should not pose a problem: https://elixirforum.com/t/return-different-values-depending-on-number-of-times-a-function-is-called/35164
  2. Use meck. Creating a wrapper around it to make it more "Elixiry": https://stackoverflow.com/a/64564727/1337392

I was not aware of meck.seq, but that sounds really useful and powerful.

If you were to submit a PR with a light wrapper around it, I'd definitely support it! 💯

Working on a PR. It's a very thin wrapper. But not too hard if we document what needs to be done.

Awesome. Thanks for putting this together @tpitale!