Before I had my first real job, most projects I worked on were just for me, like the chat "app" I built that was only ever used by me and my family.
There weren’t other developers that I was working with, and no one cared how maintainable my code was. However, in the "real world", you learn pretty quickly how important it is to write easily testable code.
What is dependency injection?
Dependency injection is a fancy way of saying "functions/objects should have the variables they depend on passed into them, instead of constructing it themselves." To best understand it, let’s look at an example:
That function seems simple enough, but what if we wanted to test it? It’s actually a little tricky to test because the date keeps changing. We could get the date ourselves and use that to compare, but then we are basically rewriting the function.
What if we rewrote our function like this:
It looks very similar, but the function we are testing now takes in a clock. The main advantage here is that we can create our own Clock to test with (commonly called a mock, or in this case a MockClock)
And now we can easily test our function. In this case, the Clock was a dependency that was injected into our function get_formatted_date.
Wait… doesn’t this just push the problem to somewhere else?
Yes. You still need to construct a Clock and pass that in to your functions.
Typically, however, there are frameworks that will manage all that for you. If you’ve used pytest, you may be familiar with their fixtures, which are another form of dependency injection. Here’s a code snippet from the fixtures documentation:
You can see that just by adding an argument that matches a fixture’s name (order and first_entry), pytest automatically injects the return value of that method into our function. Good dependency injection frameworks won’t make you write too much "glue everything together" code.
FastAPI has its own dependency injection built in to the framework. Let’s look at the first example from their docs:
Seems straightforward, you create functions that can be injected into any route. Those two routes will now take in q, skip, and limit as query parameters.
Bundling together some query parameters is fine, but we can do a lot more. We’ll start with this example route:
This route takes in an API key via the X-Api-Key header, does a database lookup to see if it’s a valid key, and then returns the user who that key belongs to. If the key is invalid or missing, a 401 is returned.
Let’s clean this up with dependencies:
Now that we’ve defined our dependency, our route becomes really straightforward:
Under the hood, this route is checking to make sure only users with valid API keys are able to use this endpoint. Any other routes that we want to protect, we can do so by just adding that one argument.
Because FastAPI is well designed, it knows that this request needs an X-Api-Key header and it adds that to it’s OpenAPI spec for that route:
Testing FastAPI dependencies
When we talked about dependency injection at the beginning, we talked about how it both made our code clean and easier to test. So far, we’ve only talked about how to make our FastAPI code cleaner.
It turns out, FastAPI has support for overriding dependencies when testing. This allows us to specify a mock version of dependencies that will be used across all our entire project. For the API key example, in tests we can just inject a specific user_id and not have to worry about setting up the database each time.
Dependency injection lets us structure our code in a way that’s both easy to maintain and easy to test.
Used along with a framework like FastAPI, you can do things like extracting and validating a user in one line of code. And it can be reused for any route and easily mocked in tests.
At PropelAuth, as an example, we used dependencies in our FastAPI library to encapsulate all our auth logic so a developer just needs to add one line of code and validating users/organizations are all taken care of for you.