Hi @dimitarvp, biggest win compared to Mox (up to Efx the best option) is no hassle with configurations and precondition that boundary needs to have behaviour. In testing, you bind your boundary functions and you are good to go.
Efx uses the fairly new ancestor-keys from the process dictionary to track down the bindings for tests event for child-processes and therefore, more tests can be implemented asynchronously.
I glanced over Efx and yeah, just as I expected, it’s another way to NOT do proper dependency injection. Why are we so afraid to code against protocols and pass around structs which implement them?
protocols are about polymorphism based on the struct type, whereas we’re discussing behavior that depends on the environment. For example, in test env we may want to avoid making real HTTP calls or spawning real processes.
Mox is a great fit when you’re abstracting an external dependency behind a behavior. But for testing internal implementations, I find e.g. Mimic much more convenient. It lets you mock internal modules without adding extra behaviors, configuration, or maintenance overhead
HTTP call or spawning a process is already a dependency. You fall prey of the chicken-egg problem. It not that mox enforces you to introduce behaviours where they don’t belong. On the contrary, it highlights the places where the dependency injection must have happened.
We migrated to Req when it appeared in 10 minutes, because—guess what—our HTTP calls were already abstracted.
That is exactly what Moxintentionallyprevents. Not because they could not figure it out. Because it improves the code quality, while adaptive, indiscriminate, and promiscuous mocking library welcomes God spaghetti objects in your code.
Exactly. The best feature of mox is not it helps to test stuff, but rather it literally points out to the places in the code that were forgotten to be decoupled with dependency injections.
Libraries being so friendly to swallow whatever people want to mock are no better than just marking tests as @tag skip: true on that matter.
The mantra that protocols are about data really doesn’t make sense to me. Implementers of a protocol can also depend on the environment… Maybe I should write a blog post about it. Seems like there are some deficiency in the community regarding dependency injection.
If you mention “dependency injection is a boundary allowing to easier decouple the code” there, I would be super-grateful because I have it (writing such a text) on my to-do list for months already.
That’s interesting - so in other words do u think I should abstract away e.g. GenServer?
e.g.
# moduleA
def foo_a(...) do
ModuleB.fun_b(...)
|> actiona
|> actionb
|> ...
end
#moduleb
use GenServer
def fun_b(....) do
GenServer.call(...)
end
def init(..) ...
In the example above I usually stub return data from moduleB.fun_b to write unit tests fro moduleA
Mimic allows me to do it without configuration burden, but I see that using mock implementation for GenServer while testing may work too - GenServer has already behavior - it is enough to mock it’s functions.
The only problem I see it to not easy distinguish of GenServer calls - as we would have to differ them based on pattern matching args which might be not very readable
another problem is deep call sequence - we would have to stub all GenServer call/cast down in call stack - where with Mimic we can isolate it right away in the testing module without worring about dependencies very deep down in the code
please correct me if I missed sth
You can’t pass something different than the implementer as first argument when implementing a protocol.
Also, why would you stub a GenServer? Just spin one in your test and pass the pid to your function or whatever. This tells me there might be bigger, more general, problems in the code base.
Are we using OTP to write procedural code, with god objects and all?
I know The question was - why I should use protocol and passing envs to it - instead of behavior and use Mox?
It’s not a good idea for unit tests. Spawning a process often involves database calls or dependencies on other processes, which can force tests to use async: false and require a much more complex setup. That’s unnecessary overhead when you’re writing unit tests
I am not sure how you came from stubbing moduleB to mocking the GenServer. Let’s make a step back and discuss the business process. You have a surfacing GenServer that relies on the implementation of some functionality hidden behind the moduleB façade.
The proper design is the GenServer is not tied to moduleB but rather implements a process layer above it. That evidently means moduleB has to be a dependency. Just do @foo_handler Application.compile_env(:foo_app, :foo_handler, ModuleB) if you want a static linking, or pass foo_handler: ModuleB to foo_a if you want it dynamic.