Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Usage of Mox (Extends #241) #355

Closed
absolutejam opened this issue Feb 8, 2020 · 4 comments
Closed

Usage of Mox (Extends #241) #355

absolutejam opened this issue Feb 8, 2020 · 4 comments
Labels

Comments

@absolutejam
Copy link

absolutejam commented Feb 8, 2020

Howdy!

I'm writing a small API wrapper and over the last couple of days, I tried to devise a pattern wherein I can reasonably test certain API calls for both happy & sad paths. I initially tried Tesla.Mock, but because this relies heavily on pattern matching the input request, I found it near impossible to dynamically change the response without external state.

The next thing I looked at was abstracting Tesla into a generic Http layer and switch this out with a mock implementation in test, but it seemed overkill as I was essentially replicating the Tesla API for no benefit. Then I found Mox, which essentially does this and #241 which starts to cover some examples.

As not to pollute what is already a busy issue, I was hoping that we could start a discussion - as long as that's not inappropriate for an 'issue' - and cover some examples and possibly outline the benefits & pitfalls.

For example, I saw the examples in #241, namely

Mox.defmock(MyApp.NiceApi.Mock, for: Tesla.Adapter)
Mox.defmock(MyApp.EvilApi.Mock, for: Tesla.Adapter)

# config/test.exs
config :tesla, MyApp.NiceApi, adapter: MyApp.NiceApi.Mock
config :tesla, MyApp.EvilApi, adapter: MyApp.EvilApi.Mock

and the later note

only works if you use module-based clients (with use Tesla), otherwise you need to handle adapter configuration yourself. I can't tell any more without RestClients.ExternalService source.

and wasn't sure about the way to make it work for my usage, which is modules that only use Tesla.{get,post,...} instead of the use Tesla approach (if there's even a difference?).

Additionally, I wasn't sure what the MyApp.{Nice,Evil}Api.Mock implementations would look like, or if they are simply dummies implementing @behaviour Tesla.Adapter as the actual mocking is occurring inside of the expect call?

@teamon
Copy link
Member

teamon commented Feb 10, 2020

If you use Tesla.client(...) you need to have something like this:

defmodule MyClient do
  @adapter Application.get_env(:myapp, __MODULE__, [])[:adapter] || Tesla.Adapter.Mint
  
  def new(...) do
    # ...
    Tesla.client(middlewares, adapter)
  end
end

as the config :tesla, MyApp.NiceApi, adapter: MyApp.NiceApi.Mock works only for module-based clients.

The MyApp.NiceApi.Mock implementation is prepared entirely by mox, using the Tesla.Adapter behaviour so you can mock calls to call/2, like in the #241.

On the other hand, you can get quite far with static mocks. In a recent project I'm doing just that, with:

defmodule FakeAdapter do
  def call(%{url: "https://example.com/stuff/1"}, _) do
    {:ok, json(%{"data" => "ok"})}
  end
  
  def call(%{url: "https://example.com/stuff/404"}, _) do
    {:ok, json(%{"data" => "not_found"}, status: 404)}
  end
end

So basically use some predefined IDs to return a different response.
It's not ideal and does have its limits, but the huge benefit is that it's stateless, there is no need for any setup before test, and you can run this is parallel without any issues.

@absolutejam
Copy link
Author

absolutejam commented Feb 10, 2020

Thanks for the reply!

I actually managed to get a rare 2 minutes over the weekend and had a brief play with this.

Looks like my knowledge of Mox was very lacking, and now I understand that when you defmock against a behaviour (in this case, Tesla.Adapter), the expect call will provide that implementation, so the MyApp.Foo.Mock module (which could be any arbitrary name), is just the mocked version that you're injecting at that time.

Starting out, I've been using

# test_helper.exs
Mox.defmock(Tesla.MockAdapter, for: Tesla.Adapter)

# inside my test
setup do
  Application.put_env(:tesla, :adapter, Tesla.MockAdapter)
end

# then inside the test cases...
# Starting with a dumb mock that matches everything
Tesla.MockAdapter
|> expect(:call, fn _env, _opts ->
    {:ok, %Tesla.Env{status: 200, body: my_example_data()}}
end)

And so far this has worked across multiple concurrent tests, wherein I can return different data in each test (eg. one test with good data, one returning bad data). Initially I was concerned running async: true would cause issues but it looks like Mox scopes to the current process.

@teamon
Copy link
Member

teamon commented Feb 10, 2020

Don't put Application.put_env(:tesla, :adapter, Tesla.MockAdapter) inside setup, put it in config/test.exs. It's static config, no need to run it on every single test, plus, if you put two different values in two different test files you will get random hard to debug errors and you won't have that problem with config/test.exs

@absolutejam
Copy link
Author

Yeah, this was just while I was throwing around a few things to see how it worked, but thanks for the guidance.

Really appreciate your input to this issue as well as your work on Tesla 👍

@teamon teamon closed this as completed Mar 3, 2020
@teamon teamon added the question label Mar 3, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants