tesla
tesla copied to clipboard
Tesla.Mock not compatible with Tesla.Middleware.Timeout
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:
- very dirty: change the implementation of the
Timeout
middleware to copy the mock into theTask
's process dictionary - dirty: change the implementation of the
Timeout
middleware to keep the parent process pid in the process dictionary; theTesla.Mock.pdict_get()
will use that to look for the mock function in the parent's process dictionary - 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.
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.
@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.
Any news on this? Having the same problem with the timeout middleware when using the Mock.
Having same problem with the timeout middleware when using Mock
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
ifMix.env == :test
(not the best idea)
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 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.
Let me know if it makes sense and I could PR on this.
PRs are always welcomed!
Here it goes, dude! https://github.com/elixir-tesla/tesla/pull/668