dashbitco / mox

Mocks and explicit contracts in Elixir
1.35k stars 77 forks source link

Adds stub_with function for stubbing a mock with module. #34

Closed bundacia closed 6 years ago

bundacia commented 6 years ago

When using Mox it can be hard, once we're using a mock in the test environment, to replace the mock with the "real" implementation in order to do any type of integration testing. For example, say we have an umbrella app with a storage app and a web app. web provides a web interface and communicates to storage via a defined behaviour to access the data layer. In order to unit test web we use Mox to replace Storage with MockStorage. But we also want to have a few integration tests that verify the integration of these two apps. The problem is that once MockStorage is in place we can't switch back to Storage. We need a way to stub MockStorage and tell it "act just like the real Storage in this test", without having to explicitly stub every function in the behaviour like this:

MockStorage 
|> stub(:fun1, &Storage.fun1/2)
|> stub(:fun1, &Storage.fun1/3)
|> stub(:fun2, &Storage.fun2/1)
#etc...

This pr adds a stub_with/2 function to Mox that provides that behaviour, allowing us to just say:

MockStorage |> stub_with(Storage)

stub_with finds all of the behaviours MockStroage and Stroage have in common and stubs all of those functions with the implementations from Stroage.

whatyouhide commented 6 years ago

@bundacia maybe I am missing where the problem is here, but how are you switching up which implementation of the behaviour you use? What I mean is, for example, you can have this in the application environment:

# In test_helper.exs
Application.put_env(:my_app, :storage_module, MockStorage)

and then you read this at runtime in the modules that use the storage, then you can just switch back to the real storage with a similar call to the one above?

bundacia commented 6 years ago

Currently we set these application config values in the config/test.exs. We could use Application.put_env to switch between the mock and the "real" implementation, but it's a little messy and requires us to remember to set the env back (so the Mox mock is in place for use by the next tests):

setup_all do
  old_storage = Application.get_env(:my_app, :storage_module)
  Application.put_env(:my_app, :storage_module, Storage)

  on_exit fn ->
    Application.put_env(:my_app, :storage_module, old_storage)
  end
  :ok
 end

It works (and is what we're doing now), but this solution seemed both simpler and more flexible. With stub_with, for example, we could configure the mock to use Storage but still make custom expectations, allowing us to provide custom behaviour for just one or more functions in the mock or even just assert that a function was called with certain arguments:

MockStorage
|> stub_with(Storage)
|> expect(:fetch_by_id, fn 123 -> %{id: 123, color: "blue"})

# or

MockStorage
|> stub_with(Storage)
|> expect(:fetch_by_id, fn 123 -> Stroage.fetch_by_id(123))

Does that make sense?

josevalim commented 6 years ago

Another advantage of not swapping the environment variable is that you can still have concurrent tests.

whatyouhide commented 6 years ago

Fair enough. I still am not a fan of having stub_with(MockStorage, Storage) and then overriding single functions though, it feels weird but I can't articulate why, so not a strong 👎 :)

bundacia commented 6 years ago

@josevalim, right. That too.

@whatyouhide, yeah, I don't have a current use case for the "stub the whole thing than add an expectation", so I can't defend that particular case very strongly, it's just something that we're getting for free with stub_with. The primary value for me here is that this is a little simpler than messing with put_env and seems a more appropriate abstraction (and is safe to do concurrently).

bundacia commented 6 years ago

Also, thanks for the refactor! I'm new to elixir and didn't know about :erlang.make_fun or function_exported?, so that was very educational =)