Talks

Injecting Dependencies for Fun and Profit

Injecting Dependencies for Fun and Profit

by Chris Hoffman

The video titled "Injecting Dependencies for Fun and Profit" by Chris Hoffman, presented at RubyConf 2019, delves into the concept of dependency injection (DI) as a method to enhance code maintainability, testability, and overall user experience. Hoffman begins by introducing the technique of dependency injection with practical code examples, emphasizing its benefits compared to traditional Ruby testing methods like partial mocking. He notes that dependency injection is a lightweight technique that can be incrementally adopted, rather than the more complex frameworks that can complicate debugging and application structure.

Key points discussed include:

- Introduction to Dependency Injection: Hoffman defines dependency injection and clarifies that it encompasses more than just one technique, dismissing the notion of dependency injection frameworks being universally applicable.
- Benefits of Dependency Injection: The presenter outlines how DI improves code maintainability, reduces testing issues, and leads to better user experiences.
- Home Automation Example: Hoffman uses a home automation scenario to illustrate how to implement DI in Ruby code, demonstrating the process of creating a simple system to report weather conditions smartly.
- Testing Considerations: He contrasts dependency injection with partial mocking, showcasing how DI allows for more controlled testing environments. By injecting dependencies, tests can simulate various scenarios, including failure states, without invoking real APIs or services.
- Failure Handling and User Experience: The importance of designing for failure is emphasized, advocating that developers need to think about how users experience failures in their applications and how to communicate these effectively.
- Rails Implementation: The talk concludes with a discussion about the application of dependency injection within Rails applications, addressing some limitations and offering recommendations for best practices.

Key Takeaways from the presentation include:

- Failure paths must be considered as part of the overall user experience.

- Explicitly defining dependencies aids in understanding codebases and improves onboarding for new developers.

- Dependency injection can be effectively utilized in Rails, enhancing code usability and testing processes.
Hoffman wraps up by encouraging attendees to consider the discussed principles when working with their codebases to facilitate better software development practices.

00:00:12.420 Okay, my time, we're starting, so that means we're going to get started. Hi, I'm Chris Hoffman, and I use he/him pronouns. I'm a senior developer at an e-commerce company called Outpour, and today we're going to talk about dependency injection. First, I'm going to introduce dependency injection with some actual code so you can get a concrete idea of what using the technique looks like. With that context established, I'm going to talk about the benefits of dependency injection, both for our code and for the user experience of our code. I'm also going to explain why you might prefer dependency injection to the more familiar Ruby testing technique of partial mocking. Then, I'm going to wrap up by examining how we might go about introducing dependency injection into a Rails app.
00:00:56.590 To start, the phrase 'dependency injection' isn’t used to mean just one thing; it's not a single technique. One of the things it can refer to is dependency injection frameworks, which are not, shall we say, universally loved. They can frequently be frustrating to work with and confusing to debug, and for applications that rely on them, it's almost impossible to escape using them. Fortunately, we're not going to be talking about those at all today.
00:01:03.129 The dependency injection I'm going to show you is a lightweight technique that you can introduce into your applications incrementally. It’ll help you write more testable and maintainable code and will help you ship software that better enhances user experience. Those are bold claims, to be sure. Even worse, I'm not going to defend those claims right now; I want to show you all code right away so that when I do defend those claims, they’ll be contextualized in what you’ve just seen instead of being baseless and abstracted from anything concrete. So with that out of the way, let's talk about home automation and Ruby.
00:01:30.459 Let’s pretend we want to automate our home and we want to use Ruby. Are we all pretending? I’m going to pretend that we're all pretending. Let's start with something simple: sometime in the morning, either right after we get up or before we leave the house, we want to know what the day's weather is. Because we don’t want to deal with a screen or having to find a screen to look at, we want to have our house read us the weather report in a non-creepy voice. So our code might look something like this: we’re going to get a prediction from a weather service, and we’re going to call it 'predict today’s weather.' This thing is probably going to talk to an HTTP API, maybe more than one if it's got some fallback behaviors.
00:01:49.100 Then, we’re going to have our house voice—I can’t call it a speaker because ‘speaker’ means literally any device that turns electrical signals into sound, not just spoken words—so that's annoying. Thank you, English! It will speak a sentence for us, and because the prediction comes from our weather service, we don’t want it to know how to turn itself into a spoken sentence. So we're going to use an intermediate object that's going to wrap that, and I think you know 'speech' is a decent home for this. This is going to be today's weather speech, and it's going to take our prediction, and we’re going to call perform on it. This is pretty much your code, but let's turn it into something that might look a little more realistic.
00:02:29.860 Assuming we're doing this in Ruby, we're probably going to be using a framework—either an open-source one or one we’ve devised ourselves. That means inheriting from a base class and defining one or more methods, like ‘smart_home_action’ and ‘perform.’ These are not immensely inventive names, but in my opinion, framework-based classes should probably be named boring things. The way we would use this, assuming our smart home library has some kind of registration mechanism, is by instantiating these globals. We create our weather service with ‘weather.example.com’ and we create a house voice with 'new'. We can choose a voice actor—let's pick Nicholas Cage; because honestly, if I want something to get me excited about the day, the antics of Nicholas Cage will definitely do that!
00:03:08.600 As previously stated in the specifications, we want the voice to not be creepy at all. So now we’re going to tell our smart home to schedule at 6:15; that’s a good time for Nick. We're going to instantiate a 'DescribeToday'sWeather' object; we're going to call 'new' or 'perform' on it. Okay, so let’s think about how we would test this. We run into two problems immediately: one, talking to the weather service in our test is potentially, at worst, going to be slow because the requests are going to succeed, and at best, it's going to make our test suite fail for reasons that have nothing to do with our code if those HTTP requests fail. Personally, I like to have a test suite that only fails when I have messed up, not because of the APIs I depend on. The house voice is even worse; we definitely do not want to use our actual house speaker system when running tests. One, that would be annoying; we might be listening to music or there might be other voice actions that we've already registered—things that tell us if someone’s at the door or that our laundry is ready. It gets worse if 'house_voice_says' is a blocking call; speech is slow compared to a computer, so we would slow down our test suite even worse than the HTTP requests that hit our weather service.
00:04:36.850 And if 'house_voice_says' is non-blocking, then it would queue up all our voice samples at once, and they would play all together, leading to a cacophonous and loud mess. So we're going to try to not use either the real internet or the real speakers while testing this. So how do we go about doing this? There are two ways: one is partial mocking, which we’ll discuss later, and the second is dependency injection. Welcome to the talk! To inject our dependencies, we’re going to create a constructor for this class. We’re going to give it some named parameters, and then we're going to assign those to instance variables. Pretty simple to do! We’re then going to turn our globals in our code into those instance variables; pretty cut-and-dried, not very complicated at all. Then, because it’s slide code and I need to save as much real estate as possible, we’re going to collapse our constructor into a single line—no information has been lost because it looks a lot like boilerplate.
00:06:11.100 Now we just need to fix our registration code to work with the new code we've written. We’re going to wrap or move our blocks around, we’ll make them local variables, and we’re going to inject them. Okay, so now this is what this would look like and we can get started testing it. We want to write our first test, which tells us about today's weather prediction. We’re going to create an instance double of our weather service and an instance double of our house voice. For those of you not familiar with the instance double method, it generates an RSpec verifying double. RSpec verifying doubles are great and play an important role in our tests. One of the important things to keep track of when testing dependency-injected code is to ensure that the non-real-life objects we’re injecting in place of our real-life objects behave the same way.
00:07:27.430 That means we want them to have the methods we're going to call, and those methods need to have the same argument signature, the same return type, and if possible, they should raise the same exceptions. If you don't, you might end up with a test that repeatedly fails with a 'no method' error, or worse, you could have a test that passes for the wrong reasons. If we were working in a language like Java, we would use interfaces and have the compiler take care of verifying all of that for us. Well, we don't have interfaces or a compiler in Ruby; fortunately, these doubles are going to help us. Right now, this code will not do anything. I mean that literally—if I call 'predict_today's_weather' on our weather service double, even though it's a double of the weather service class, it will give me a 'no method' error. So if I want this test to do anything, we have to start configuring our doubles.
00:08:35.700 So, first, I’m going to allow the weather service object to receive ‘predict today’s weather’ method, just like it does in our code above, and I want it to return some data. In this case, it’s very simplified; it would also give me the precipitation percentages and a bunch of other things, but again, let’s keep it simple for this example. This allowance is very cool, but it still does not create a test because this doesn’t make assertions. To do that, I’m going to expect our house voice to receive 'say', just like it does in the code, and even better, I can make it only pass our tests if it receives the exact string fragment matching what I am going to assume is the output of our 'today's weather speech'. So now, this test would fail because nothing in it is calling the 'perform' method in our class.
00:09:53.120 We’re going to create a 'DescribeToday’sWeather' object; we’re going to give it our weather service, we’re going to give our house voice (thanks, Nick), and we’re going to call 'perform' on it. This test will now satisfy the method expectation we’ve placed on our house voice double. This is one of the main benefits of dependency injection; it allows us to test the logic and interactions of our code with precise control over what side effects, if any, will happen during our tests. It's also very straightforward to use. We have our arrangement pattern—set up our doubles, configure them, call our code, and pray that it works. In this case, it does, so it couldn’t be more straightforward.
00:10:58.690 But there is more! Let’s talk about failure. We don't often design for failure that well; we don’t often consider it a core component of user experience, but we're wrong when we do that. When a user encounters a failure state in your system, they are at their most confused and least empowered. Because of that, it’s crucial that as developers, we do two things: we must communicate to them what has happened, and we can also tell them what, if anything, they can do about it regarding what we are doing about it.
00:11:09.950 In our code, if it doesn’t run anywhere because it doesn’t exist, failure has no consequences whatsoever. However, we work on things with actual users that have real consequences when things fail. So let’s consider what happens if our weather service can’t get a weather prediction. Let's pretend our weather service will raise a ‘NoPredictionAvailable’ error if it cannot talk to its website or if it times out or if a variety of things occur. In this case, we would like Nick to read a default message to us, and yes, we could make a '2001: A Space Odyssey' joke here. But as previously mentioned, we want the voice to not be creepy, so there will be no references to any Daves or inability to perform things on Dave's behalf!
00:11:23.930 To test this, we pretty much do the same thing. We’re going to test that it informs us it can’t get a weather prediction. We’re going to collapse the initializer again and make our weather service and our house voice. We're going to allow the weather service to receive ‘predict today’s weather’, just like before, but in this case, we don’t want it to return anything; we want it to raise a ‘NoPredictionAvailableException’, just like our code does. Just as before, we’re going to expect our house voice to receive 'say' with a fragment that contains the words 'unable to predict' in order with spaces between them. This isn’t just a simplification for slide code; I often use regex to match strings because if I just match the entire string, anytime I change the string due to an adjustment in tone by our designer, which is a good thing to do, I don’t have to redo my whole test.
00:12:50.560 So right now, the important bits we care about are 'unable to predict' and just like before, we’re going to create our 'DescribeToday’sWeather' object, pass it our dependencies, and call 'perform'. The only difference between this test and the previous one is these few bits. As dependency injection is a great way to control testing of the logic and interactions of our code while managing the side effects, it’s also just as straightforward to test sad paths as it is to test happy paths. If we want to start designing our failure behaviors to provide a better user experience, dependency injection is really the way to go.
00:14:00.190 I'm going to address a question that I'm sure has occurred to a lot of you: why can’t we just partially mock all of this stuff? After all, here’s our original code with globals, and I’ve updated it to include the failure behavior we just added. Here’s how we would test it. We would allow our weather service to receive ‘predict today’s weather’; we don’t need to set up test doubles because they already exist. They’re globals, but we do need to change how they work. We want it to receive a ‘NoPredictionAvailable’ and we want to expect our house voice to receive ‘say’ with ‘unable to predict’, just like before.
00:14:31.010 Now, not all instance doubles are RSpec; RSpec is powerful enough to override existing objects that it doesn't control. We call it partial mocking, but really it’s monkey patching, at least in testing. So, that’s what we’re doing. Just like before, we’re going to have to instantiate ‘DescribeToday’sWeather.new.perform’. Now we can absolutely test this piece of code roughly the same way we did with the dependency-injected piece of code, and this is less code than before. The question isn’t—let’s say the question is: Why should I use dependency injection instead of partial mocking? The question being asked is why should I change my code to make injecting dependencies easy?
00:15:45.640 The reason for that is, like I said, the difference between these pieces of code isn’t the testing techniques. The difference lies in how we manage those dependencies. One of these has an initializer, and one doesn't; one has explicit listings of all its dependencies in one convenient location that anyone can read, and the other doesn’t. Although there is more code in the dependency-injected version, that code often gets dismissed as boilerplate. It serves a critical function that more than makes up for the extra line count.
00:16:41.760 Being able to glance and know what the dependencies of a given object are is exceedingly powerful in software development. It helps you answer questions like, ‘Is this code doing too much?’, which we normally answer with a gut reaction. Codebases get large, and we think, 'Well, this thing does that and it does the other thing, it talks to people’s maps and it talks to this coupon API, and it does this fourth thing, but it also makes our robot do a thing.' We’re probably going to miss three dependencies with just a gut reaction; if we have a list of them, it’s much easier to spot. We can also answer the question: How can this thing fail? This is also an important part of having a good user experience.
00:17:17.460 In large codebases, it’s hard to get an accurate read of the code. However, with our initializer, we can simply look at it and know what our dependencies are. It helps onboard developers faster by acting as an information display, so they don’t have to hunt through code to figure out what all the responsibilities of an object are. Quite frequently, those responsibilities closely mirror the dependencies. It also prevents surprises from cropping up during testing. You know that feeling of testing a large piece of code and thinking you’ve stubbed or mocked or faked all dependencies, yet you’re surprised when your test doesn’t pass? That feeling is awful! Having an explicit listing of our dependencies helps to avoid that surprise.
00:18:11.500 But even worse, when we’re surprised by our dependencies, it keeps the knowledge of how to test or what that code does in one person’s head. If you have a large codebase, and dependencies are all over the place, you may eventually get that code tested because your job relies on it. But once you’ve done that, the knowledge stays with you or your pair's head if you're pair programming, whereas with dependency injection, that knowledge is always present in the code, and anyone can look at it and find it.
00:19:38.120 The reason to use dependency injection instead of partial mocking is that it enables you to know your code in a way that partial mocking doesn’t. So now that sounds great if a little apocalyptic for those of us who aren't using it already. But how do I do this in Rails? I know we are at RubyConf, so let’s talk about it in Rails. A disclaimer before we begin: if any of you were hoping I’d show you some amazing way to inject a stub of ActiveRecord so you could test Rails code without talking to the database, I’m really sorry. That ship has already sailed.
00:20:27.990 It probably wouldn't take you too much time to create a stand-in that would let you mock or fake create, save, update, delete records, but as soon as you want to test any transactional behavior or any of the more complicated concurrency control mechanisms, you’re essentially committed to building an in-memory database. We already have one of those called SQLite, but it doesn't implement all the concurrency control behaviors of the SQL specification. If you want to spend more engineering time than the maintainers of SQLite have, feel free! If you do, I will use it.
00:20:58.500 It’s much the same way with the components provided directly by Rails. One of the things you may have noticed is that in order to use dependency injection, we have to control object instantiation. That is 100 percent true; but I said I wasn’t going to talk about dependency injection frameworks. So we have to control object instantiation. Rails doesn’t really let us control object instantiation at all, except with ActiveRecord, and with ActiveRecord, all we’re doing is providing data.
00:21:58.810 That means we don’t provide a new database connection for each user object we create, which doesn’t really allow us to substitute dependencies. Fortunately, we can use dependency injection with service objects or interactors, or whatever variant you wish to call the variant you’re working with. We control the creation of those objects, so we inject your own dependencies in real life and in tests. Here’s an example of what our weather prediction might look like if we used it in Rails. Obviously, Nic Cage doesn't scale much; however, we’re going to email our weather predictions instead of having Olmec read them out. Yeah, I wanted to see how the audio API worked too, but that isn't the focus of this talk.
00:23:08.420 Like before, we’re going to have an initializer with our weather service and a user because we want to email a specific user with the weather service. Maybe our user is Nicolas Cage—that would be cool! We're going to create a call method like before. We will have our weather service call ‘predict today’s weather.’ We’re going to instantiate our weather prediction service with our weather service and the user, and we’re going to call it. Then we will do one of the many ways you can test that a mail was delivered in Rails.
00:23:50.140 So again, while we can't use dependency injection throughout the entirety of our Rails code, we can create a code that is separate from the standard Rails nouns it provides us and control them ourselves in controllers or models and use dependency injection on them. To recap, I've demonstrated what simple dependency injection looks like and how it's straightforward. I've explained its benefits and what advantages it has over partial mocking. I’ve shown what using dependency injection in a Rails application might look like.
00:24:07.940 Now I might normally ask what we’ve learned, but I have no idea what you all have learned! For some of you, this has all been mostly new, and for some of you, it’s mostly not—in fact, if you’re one of the people who have demoed this talk to you, not only did you learn nothing, you also knew all the jokes I was going to say! So instead of asking what we’ve learned, let’s talk about what I think you should take away from this talk since, even if you didn’t learn a thing, that’s still a relevant question.
00:25:00.000 I hope that you take away three things from this talk. One: sad paths are part of your UX; they are not a thing you can ignore if you want a truly great user experience. And as developers, once we have thought about and specified that failure behavior, we should be testing it. The second thing is that explicit dependencies help you know your code. They are invaluable in small and large codebases; they let you test the failure behavior we just discussed and help onboard new developers. They reduce confusion and surprise and frustration throughout your codebase—they are an unalloyed good thing. The third takeaway is that dependency injection is possible and useful in Rails. This is not just a Ruby testing technique but rather a technique that can be successfully employed in our Rails applications.
00:25:50.880 Thank you! Now, does anyone have any questions? If you have a codebase that’s fully dependency-injected, do we ever have composite classes to merge some of those dependencies so we’re not passing a bunch of them around all the time? That's your question, right? Okay, so when I do this in actual codebases, I generally don’t have a named parameter for each dependency. What I’ll frequently do is have some kind of object, like an open struct or a hash that I attach a bunch of dependencies to. Each object that will be dependency-injected takes ‘deps’ as a keyword. In their initializer, they will assert the dependencies they need.
00:26:20.170 So we retain all the information that our constructor gives us, and we also avoid the problem of needing to inject a dependency five layers in. So that works well! That is what the instance doubles are for. Instance doubles in Ruby automatically verify themselves. If I try to allow the ‘weather_service’ to receive ‘say’ when it doesn’t implement it, RSpec will throw a ‘no method’ error! If I try to expect 'house voice' to receive 'say' with no arguments while it has at least one positional required argument, it gives me an argument error.
00:26:53.240 When it comes to class doubles, I don’t use them all that frequently, as I prefer my code structure to feature instances rather than class-based architecture. However, if I ever have singleton or class methods that need dependencies, I’ll resort to class doubles. But generally speaking, I structure my code to avoid needing that. If you want to verify returned doubles that are the same as the returned values, the answer is you can’t because we don’t have a compiler and interfaces in Ruby. These are static data that you can access through Ruby introspection, but the actual return value is not something we can verify directly without manually checking.
00:28:38.680 That being said, I haven’t done much with Sorbet, but that might be something to investigate if you really want to go down that path. Ultimately, you will need to look at the return values with your human eyes and verify if it’s indeed what you expect.
00:28:40.840 If you have any further questions, that’s my Twitter handle—or you can find me at the conference. I regret to inform you that I am not tall, but I do have this ponytail, so maybe that’ll help you find me.