Example: An API Client
Most client applications need to communicate with a remote server, So let’s build a simple API client module with a few functions. The end goal looks like this:
Our client is just a namespace of functions, each one representing a different HTTP request to a remote server. To send the actual request we intend to use fetch which returns a Promise wrapping the response. So the fetch function will be our dependency.
Starting With Tests
Let’s write a few unit test cases to drive the development of our functions. In order to test them in isolation we need to inject a fake fetch function as we don’t want to exercise the real HTTP requests.
We stubbed fetch to return an arbitrary response and passed as an argument to fetchTodos. That can be implemented as a default parameter.
We’re importing the real fetch implementation as the default argument value. This way, we keep the proposed interface unchanged. Let’s add a test for addTodo as well.
Here again, we pass the fakeFetch as an argument to addTodo. The implementation will be similar as the one for fetchTodos.
At this point, we can remove the code duplication by moving the default parameter to the namespace level. For that, we’ll define a createApiClient function. Let’s change the tests accordingly.
As you can see in the new version of the tests, we’re now injecting fakeFetch to createApiClient. That new function acts like a contructor: it builds the namespace and make the injected fetch available to the entire namespace scope. Let’s make that change to the application code.
With the refactoring above, our initial code snippet will have to change a little bit. Now we have to call first createApiClient instead of just importing all functions.
The code above represent a sample usage of our module. Notice that the calling code doesn’t have to pass fetch to createApiClient since we used a default parameter. Besides that, we are now able to add more functions to the module (like toggleTodo, for instance) and have fetch available to them.
ES6 default parameters enable functional DI in simple and elegant way. The approach demonstrated in this post suggests a new point of view on function arguments:
The regular arguments - the arguments which matter to the application code.
Dependencies - collaborator functions defaulting to the real implementation and stubbed in unit tests.