elixir-tesla / tesla

The flexible HTTP client library for Elixir, with support for middleware and multiple adapters.
MIT License
2.01k stars 349 forks source link

feat: add multiprocess support to local mocking #654

Closed jbsf2 closed 4 months ago

jbsf2 commented 9 months ago

Not sure whether this might interest you ...

This PR enables local mock adapters to be "seen" not just within the ExUnit test process, but also within processes spawned by the ExUnit test process. No code changes are required within tests that use Tesla.Mock.

This is accomplished by adding the ProcessTree library to mix.exs, and invoking ProcessTree.get/1 from within Tesla.Mock.pdict_get/0.

Via ProcessTree, code running in child processes can access data stored in the process dictionaries of parent processes, in most circumstances (as discussed further below).

As used here, the result is that processes spawned by the ExUnit test pid can "see" the local mock adapter, but other tests and their child processes cannot. async: true is preserved.

Limitations

ProcessTree is able to return a meaningful answer in most real-world circumstances. For example, when the processes in question are running in a supervision tree, ProcessTree will always be able to return an answer. Beyond that, things depend on factors including:

Further discussion can be found here: https://saltycrackers.dev/posts/how-to-get-the-parent-of-an-elixir-process/

Detection/messaging of error circumstances

Helpful error messaging is the trickiest part of this PR.

As was the case prior to this PR, if Mock.call() fails to find an adapter when the code is running within the ExUnit test process, we display the same error message as before:

          There is no mock set for process #{inspect(self())}.
          Use Tesla.Mock.mock/1 to mock HTTP requests.

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

If, however, the code calling Mock.call() is not running within the ExUnit test process, that can mean one of three things:

  1. A mock has not been set up at all
  2. The test spawns or manages process in a nonstandard or unusual way that's incompatible with ProcessTree, and Mock.call() has been called within such a process.
  3. Mock.call() has been called within a process not spawned by the ExUnit test pid.

We're unable to distinguish between these situations, so the error messaging is a bit awkward - I've given it my best shot for your review.

tomekowal commented 8 months ago

This is gold! 🥇 I recently learned about ProcessTree library and was figuring out if we can use it with Tesla (our Phoenix controllers schedule Oban jobs to make further http calls). AFAIU, Oban spawns jobs as children of the test process in testing mode. With this PR, I could simply change mock_global to mock and async: false to async: true and speed up tests with very little effort!