tesla icon indicating copy to clipboard operation
tesla copied to clipboard

Tesla.Mock not compatible with Tesla.Middleware.Timeout

Open stefanchrobot opened this issue 7 years ago • 5 comments

Tesla.Mock is not compatible with clients that use the Tesla.Middleware.Timeout middleware.

iex(1)> Application.put_env :tesla, :adapter, :mock
iex(2)> require Tesla
iex(3)> Tesla.Mock.mock(fn env -> %{env | status: 200} end)
iex(4)> client = Tesla.build_client([{Tesla.Middleware.Timeout, timeout: 2_000}])
iex(5)> Tesla.get(client, "/")
** (Tesla.Mock.Error) There is no mock set for process #PID<0.450.0>.
Use Tesla.Mock.mock/1 to mock HTTP requests.

See https://github.com/teamon/tesla#testing

    (tesla) lib/tesla/middleware/timeout.ex:60: Tesla.Middleware.Timeout.repass_error/1
    (tesla) lib/tesla/middleware/timeout.ex:35: Tesla.Middleware.Timeout.call/3

The reason is that Tesla.Mock looks for the mock in the current process' dictionary and the Timeout middleware runs the Tesla pipeline in a separate Task to be able to terminate it on timeout.

As a workaround, I'm going to disable the Timeout middleware in the test env, but this is kludgy. I'd rather have the mock support the Timeout middleware.

I see three ways to do this:

  1. very dirty: change the implementation of the Timeout middleware to copy the mock into the Task's process dictionary
  2. dirty: change the implementation of the Timeout middleware to keep the parent process pid in the process dictionary; the Tesla.Mock.pdict_get() will use that to look for the mock function in the parent's process dictionary
  3. clean: change the API of the mock spec so that it doesn't land in the process dictionary:
client = Tesla.Mock.mock(client, fn -> ... end)
module = Tesla.Mock.mock(MyApi, fn -> ... end)

The mock function would put the mock spec somewhere in the middlewares. Ideally, this would replace the current adapter with specced mock adapter - this has the added benefit that one no longer needs to set the :mock adapter in tests.

The last point would actually be great thing to do, since I have the following use case: I have a set of unit tests for my API module where I mock the adapter and verify it's behavior. But for all other tests I've built a fake implementation of the API which is started alongside the main app - this way the tests exercise the same code path as the production code. For that reason, I would strongly prefer solution 3.

Let me know which one works for you and I'll happily prepare a PR.

stefanchrobot avatar Jan 26 '18 12:01 stefanchrobot

This is definitely a bug, thanks for reporting!

About the client/module approach:

  • you can already use it today - https://github.com/teamon/tesla/blob/fec8b6c891ae8942919de7ca2f3a8f196aa62228/test/tesla_test.exs#L217-L224
  • it only works when calling the client module directly and makes testing e.g. controllers quite hard

Nevertheless, the issue with timeout & mock is valid and needs to be somehow fixed.

teamon avatar Jan 26 '18 14:01 teamon

@stefanchrobot I would actually prefer option 1, personally.

Option 2. makes following the behavior quite murky for no reason, and you actually really only need to fix the mock situation, after that you can capture other values from the test setup in the mock closure.

Option 3. you can do already now, so there's no change needed, though it has the limitations that @teamon mentioned.

unthought avatar Feb 14 '18 17:02 unthought

Any news on this? Having the same problem with the timeout middleware when using the Mock.

vitorleal avatar Jul 07 '18 03:07 vitorleal

Having same problem with the timeout middleware when using Mock

leineu2016 avatar Aug 18 '18 18:08 leineu2016

As you can read in #255, this issue is more complex than it seems. There are two workarounds:

  • Use Tesla.Mock.mock_global/1 (preferred)
  • Or skip including Tesla.Middleware.Timeout if Mix.env == :test (not the best idea)

teamon avatar Nov 09 '18 13:11 teamon

Use task_module of Tesla.Middleware.Timeout to control the Task module https://github.com/elixir-tesla/tesla/blob/9c6d32133b8ce94fad593cad57019cfd260556f7/lib/tesla/middleware/timeout.ex#L28-L30

Swap the implementation with one that doesn't use Task

yordis avatar Sep 10 '23 09:09 yordis

@yordis even if the solution you propose will work but, I was thinking that an alternative solution could be using ancestors to find the process with the mocked fn, so we are able to solve this problem. Creating an alternative Task implementation is not very good as it requires storing the state in the process to await later for it synchronously forcing clients to do so.

Something like this should be enough to find the process that contained the mocked data:

    pid = Enum.find(Process.get(:"$ancestors", []), nil, fn ancestor ->
      !is_nil(Process.get(ancestor, :testa_mock))
    end)

Let me know if it makes sense and I could PR on this.

carrascoacd avatar Apr 10 '24 12:04 carrascoacd

Let me know if it makes sense and I could PR on this.

PRs are always welcomed!

yordis avatar Apr 10 '24 15:04 yordis

Here it goes, dude! https://github.com/elixir-tesla/tesla/pull/668

carrascoacd avatar Apr 11 '24 14:04 carrascoacd