Mox and Protocols?

Hi @peerreynders,

It seems that you solved my problem and identified the false dichotomy I felt I was making initially.
Also, you are right about the wrong type of the return value of Shortlist.put_in. Thank you.

Meanwhile, I kept experimenting with mocking with protocols (after all, the idea was suggested by @josevalim in his famous blog post “Mocks and explicit contracts”. I did find some degree of happiness with the following solution. I defined an extra property in the struct which implement the Storage protocol in tests. This enables individual tests to specify the desired return value of the protocol functions.

Example:

# PRODUCTION CODE

defprotocol Shortlist do
  def put_in(shortlist, item)
end

defmodule Logic do 
  def submit_item(item, config) do
    # ...
    Shortlist.put_in(config.shortlist, item)
    # ...
  end
end

# TESTS

defmodule Shortlist.Mock do
  defstruct [:put_in_result] # canned result specified by each test

  defimpl Shortlist do
    def put_in(shortlist_mock, item) do
      # mocking is about expectations, so this line enables message-based-expectations, see the test
      send self(), {:put_in, shortlist_mock, item}
      shortlist_mock.put_in_result
    end
  end
end

test "item not already listed" do
  config = %{
    shortlist: %Shortlist.Mock{
      # test specific canned result
      put_in_result: {:ok, %Shortlist.Mock{}
    }
  }

  an_item = ...
  Logic.submit(an_item, config)

  assert_received {:put_in, ^config.shortlist, ^an_item}
end

This solution is:

  • :+1: polymorphic
  • :+1: mockable
  • :+1: safely testable with async: true
  • :+1: it’s fast, there isn’t even a Registry process like I believe there is under the hood in Mox
  • :+1: I don’t have to wire modules (behaviour implementation and client) with config.exs, this feels very good, no need for Application.get_env(:app, :behaviour_name) scattered in the codebase, I can just pass a config (Map|Struct) argument around
  • :+1: unlike @peerreynders’s solution, every implementation details is stuffed inside a single argument by leveraging structs/protocols

This is for the “degree of happiness” mentioned above.

Now I’m not totally happy with this solution. It feels more verbose / require a bit of ceremony:

  • I don’t have an assistant like Mox:
    • :neutral_face: I don’t have a fancy equivalent of Mox.expect(module, fun_name, fn ... end) but I can make a private helper inside the test module to make this concise, explicit and readable
    • :neutral_face: I don’t have automatic verifications a-la verify_on_exit! so I have to manually verify with assert_received
  • :neutral_face: I have to define a test protocol implementation for each protocol to mock, instead of the simpler defmock MockModule, for: Behaviour, this is not as bad as it may sound though, see code above.
  • :-1::-1: I now have a mix of behaviours and protocols. My tests end up with a bit of Protocol mocking and a bit of Behaviour mocking with Mox. For example: ItemValidator is just an algorithm so I should make it a behaviour and use Mox, while Shortlist is a protocol. I thought I could make ItemValidator a protocol too, leading me to no behaviours, ultimately leading to ditching Mox. The issue I found immediately is that It forced me to create a Validator struct with no properties, because it’s just an algorithm. That’s a signal that I’m certainly pushing too hard in this direction.

This last sentence reveals my overall impression with this entire thing. I am really tempted to surrender and follow @peerreynders’s approach. Though, in theory Protocols are right-er in this context. Ditching them in favor of the more convenient behaviours doesn’t feel right either. Convenience doesn’t feel like the right reason to use behaviours instead of protocols, and if that’s the main reason then maybe a bit more of tooling a-la-Mox would change the deal?

Thoughts @peerreynders & @josevalim ?