How Dependency Injection Can Make Rails Tests Easier

by Jason Swett,

“Dependency injection” is a fancy-sounding term. When I first heard it I assumed it referred to some super-advanced technique. It wasn’t until years later that I realized that dependency injection is a pretty straightforward technique once you understand what it is.

My aim with this post is to cut through the jargon and show you in simple terms what dependency injection is and why it’s useful.

But first: why are we interested in this topic?

Why bother learning dependency injection?

Depending on how it’s written, some code can be easy to test and some code can be hard to test. Code with entangled dependencies is hard to test.

Why is code with entangled dependencies hard to test? Imagine I have a class Order that requires instances of a class called Payment in order to function. Let’s then imagine that Payment needs some PaymentType instances (Visa, MasterCard, cash, etc.) in order to work.

This means that in order to test the class I’m interested in, Order, I have to bring two other classes into the picture, Payment and PaymentType, just to perform the test. And what if Payment and PaymentType in turn depend on other classes? This test is going to potentially be very tedious to set up.

The opposite of having entangled dependencies is having loose coupling and modularity. Modular, loosely coupled code is easy to test. A number of factors have a bearing on how modular and loosely coupled your code will end up. What I want to show you right now is how dependency injection can help make your code more modular and therefore more easily testable.

An dependency-laden Rails model

Let’s say you’re working on a legacy project that you recently inherited. There’s very little test coverage. You encounter an ActiveRecord model called CustomerFile. There’s a method called parse that evidently parses a CSV.

Let’s focus on this line for a second: content = File.read(customer.csv_filename).

Apparently a CustomerFile object has an associated customer object which in turn has a csv_filename. How exactly does customer get set? It’s not clear. Where exactly is the file that customer.csv_filename points to? That’s not obvious either.

We can try to write a test for CustomerFile but it probably won’t go very well.

Our attempt to write a test hasn’t proven very fruitful. The challenge of writing a test for this class is somewhat “uncomeatable”.

The reason it’s hard to write this test is that CustomerFile has a dependency inside of a dependency. We don’t know how to make a customer, and even more problematic, we don’t know how to make a CSV file for that customer.

Applying dependency injection for easier testability

Let’s imagine now that parse doesn’t require that we have a customer with csv_filename that points to some mysterious file on the filesystem somewhere.

Let’s imagine a version of parse that just takes the file contents as an argument.

When we try to write a test now, we’ll see that it’s much easier.

In this case parse doesn’t know or care where the CSV content comes from. This means that we don’t have to bring the filesystem into the picture at all which makes writing this test very convenient. No customer object or customer.csv_filename value necessary.

If we want to use parse for real in the application we can just pass in the file contents like this: parse(File.read(customer.csv_filename)).

Conclusion

Modular, loosely coupled code is testable code. You can use dependency injection to help make your code more modular.

Leave a Reply

Your email address will not be published. Required fields are marked *