Learn With Me: Elixir - Dependency Injection and Mocking (#67)
After learning about unit testing, I was curious about the subject of dependency injection and mocking, so I took the time to read about the subject.
By the way, I'm assuming that you are familiar with the concepts of dependency injection (also called "inversion of control") and mocking. If not take a look at this article discussing dependency injection (I think the examples are in Java, but that's irrelevant) and this Stackoverflow discussion about mocking. I could write a lot about these concepts, but I want to stay focused on Elixir.
Here's my take on this from what I've read. Keep in mind that I don't yet have any practical experience.
I discovered that although the concepts of dependency injection and mocking exist in Elixir, it seems to me like these concepts aren't as emphasized in the Elixir community as they are in other communities. I think that this is because the nature of functional languages lessens the importance of these concepts.
Unlike object-oriented languages such as C#, there are no objects in Elixir and dependencies cannot be passed into a constructor, so there no chain of object construction similar to what occurs in dependency injection frameworks in object-oriented languages.
So let's think about what kind of dependencies there are in Elixir. In Elixir, a function has two types of dependencies. The first kind are the parameters. Those are determined when the function is called, so your code can be passing one parameter to the function while a unit test can pass in the parameters it needs to run the test. That's no problem for testing and no dependency injection is needed for calling a function.
The other type of dependency are the other functions that a function depends on. If your function is a pure function, this is less relevant. Pure functions don't interact with anything beyond function inputs and outputs. So for a test you can pass in a set of inputs, and receive a set of outputs, and test the results. It's that simple, and there's no need to mock any dependencies, no matter how many function dependencies there are. As long as nothing in the dependency tree creates a side effect, testing can be simple. Those sorts of functions are common in Elixir if you are attempting to isolate the side effects and use pure functions as much as possible. So that's why concepts like dependency injection and mocking dependencies are less central in Elixir than they are in a lot of other languages.
However, there are still reasons to create some kind of dependency injection and mock dependencies. From my perspective, there are two scenarios where you'll need to resort to this: when side effects are involved or when testing a pure function would be very complex and you need to simplify it.
Let's take a look at the complex test scenario. Elixir emphasizes small simple functions that are composed together in other functions. At some point you're going to have functions that are at the top of the function composition tree and testing those without any kind of dependency mocking may be very complex. Let's say for example that you have a data structure that contains the current game state. You pass the game state to a function that returns the game state for the next time slice so that the screen can be updated. This may be very complex, as the game state may be very large and there could be a lot of variations in how it is transformed. Attempting to test on its own can get very complex. Instead, it would be much easier to mock the function dependencies so that you just test what the function is doing rather than also testing what all of its dependencies are doing as well. This can greatly reduce the complexity of your tests.
Now let's look at side effects. Side effects, particularly functions that are responsible for talking to other systems, can be much more difficult to test. When you test any functions that produce side effects, you'd like to mock the function that produces the side effect to make your test a lot simpler. Then you can test the side effect separately. This is done in Elixir by "pushing side effects to the edges". This means that side effects of various kinds are isolated in their own modules and that all the functions in those modules do is perform the side effect: nothing else is there. That allows the side effects to be isolated and the code to be more easily testable by mocking the side effect functions.
Some test frameworks in dynamic languages will mock modules and functions by altering the global environment. I've certainly seen this in Javascript. This is a big no-no in Elixir.
C# takes the approach of referring to a dependency using an interface and then creating a mock object that implements the interface. This is more along the lines of how Elixir approaches the problem. Let's look at how Elixir implements dependency injection and mocking.
Dependency Injection and Mocking using Configuration
With this method, instead of referring to a module by name, you replace the module name with an attribute.
You'd take the following call:
Application.SomeService.send_data(data)
...and replace it with:
@some_service.send_data(data)
The @some_service
attribute for placeholder that is determined by the application configuration.
So once you call a function using an attribute placeholder, you'd define the module attribute with a value from the application configuration.
@some_service Application.get_env(:application, :some_service)
That would define the module name at compile time.
The application configuration in config.exs would look like this:
config :application, :some_service, Application.SomeService
So the application would be using the correct module at runtime. You'd actually need to put the configuration in the config_dev.exs and config_prod.exs files that are loaded when building for a dev or prod environment. Then when building for a test environment when running unit tests, you'd have a different configuration in a config_test.exs file that was loaded only when testing.
That test configuration would configure the module attribute to be a test module.
config :application, :some_service, Application.SomeService.MockClient
The Application.SomeService.MockClient
would be a mock module defined in the testing code. That way a mock module would be injected into the attribute during testing.
So in the example of @some_service.send_data(data)
. the dev and prod configurations would cause Application.SomeService.send_data(data)
to be called when running the application and the test configuration would cause Application.SomeService.MockClient.send_data(data)
to be called during testing.
There is a library called Mox created by José Valim that can create mock objects and verify that functions have been called. It's able to create a mock module when the original module implements a behavior. I haven't learned what a behavior is yet, but I get the impression that it's like an interface in C#. I thought protocols were like interfaces too, so that leaves me a little confused. I'll have to learn about behaviors and protocols.
Typically an entire module is injected in this manner, although I haven't seen anything that would prohibit individual functions from being injected in this manner as well. I would have to know Elixir a bit better before I could come to a conclusion about individual functions.
José Valim wrote an article that covers this approach, which I recommend reading: http://blog.plataformatec.com.br/2015/10/mocks-and-explicit-contracts/
There's also an even better article by Andrew Hao on the Carbon Five blog that is really good at talking about this: https://blog.carbonfive.com/2018/01/16/functional-mocks-with-mox-in-elixir/
A Functional Alternative - Passing Dependencies as Parameters
Rather than creating creating a dependency injection framework like I described, there's another approach. You can just take a purely functional approach and pass your dependencies in as parameters.
Typically an individual function is injected in this manner, although I haven't seen anything that would prohibit an entire module from being injected in this manner as well. I would have to know Elixir a bit better before I could come to a conclusion about that.
This article by Andrew Hao on the Carbon Five blog covers how to do this: https://blog.carbonfive.com/2018/03/19/lightweight-dependency-injection-in-elixir-without-the-tears/.
He covers passing in functions as parameters with a variety of approaches, which have various pros and cons. Andrew recommends going with the simplest solution when you only need to inject a few functions (function parameters with a default value) and then go for more complicated methods if you need to start passing in a lot of parameters.
At test time, you'd define your own mock functions and then pass those in. This feels far more suited to the scenario where you are testing a function with a high test complexity and you need a way to simplify it.
My Conclusion
It's clear to me that there's no One Right Way to do dependency injection in Elixir. It seems to greatly depend on who is doing it and what they are doing. There are also some concepts (behaviors and protocols in particular) that I'm not yet familiar with, so I don't think I fully understand this subject matter yet. I plan to learn more and then revisit this topic with specific code examples.
At this particular point, here are my recommendations.
- Don't bother with any kind of dependency injection or mocking unless side effects are involved or testing starts to get complex. Most of your testing will likely involve pure functions with simpler tests
- When testing a function gets complex due to a lot of composition within the function, turn to the dependency injection implementation where you pass in individual function dependencies as parameters. This is relatively simple and allows you to pass in mock implementations easily.
- When testing functions with side effects (particularly any interaction with another system), do module injection using behaviors and configuration. Such side effects should be isolated in their own module, making it advantageous to do this, and you won't have to do large amounts of configuration.
The articles I've read do not recommend enabling dependency injection and mocking every time another module is used, which would happen in Javascript or C#. It instead recommends doing this at context boundaries where it switches context to doing something different (such as making a call to another system or a different sub-system in the same application) that's a complex piece on its own and should be tested separately. So this is a tool that should only be used at a few strategic boundaries and not just everywhere. I imagine I'll get a better sense of this as I get more Elixir experience.
Just like Douglas MacArthur, I plan to return to this in the future, preferably after I've learned some more concepts and done some more coding.