I recently wrote some code to interface with Stripe’s webhooks. After looking at the code and tests, I decided I needed to do something to make it easier to test all pricing tiers— something I wasn’t able to easily do from the start.
Dependency injection was a necessary piece of that puzzle. I’ve always been curious about the various forms of dependency injection and the effects each would have on the code. Below I explore 2 options (constructor injection and setter injection).
In the end, setter injection felt for more natural for this case and it didn’t interfere with the classes argument list and felt ancillary to the responsibility of the code. While the change in code was small, it has a huge impact on my confidence in the code and associated tests.
The Code
The class below is responsible for handling Stripe’s invoice.created webhook. Prior to a customer being billed monthly subscription, Stripe will ping your application (if configured) — giving you the opportunity to add additional line items (think metered billing…). It could be additional services, or perhaps the entire bill itself (this use case). Nevertheless, the responsibility of the class is to create an invoice item based on the customer’s usage during the previous period.
I wrote this code pretty quickly and felt pretty good about it. The responsibility of determining the pricing tier had been broken out in to a separate class, as well as determining the customer’s actual usage. At least I thought they were…
So what about the tests?
The first thing I noticed with this setup was the detailed usage of Stripe::InvoiceItem.expects. I wasn’t sure if this was necessarily a bad thing because it was a third-party service and it seemed like reasonable boundary of the application.
Aside from the mock, another thing that bothered me was the difficulty simulating different pricing tiers and customer usage. You probably noticed the Stat.create!… in the last test. I could’ve duplicated Stat entries until I reached some arbitrary level of usage that bumped this user to the next pricing tier. But that felt risky and very dependent on knowing the actual value of the subsequent tier.
What if I wanted to change the ceiling of that tier next month? I’d have to come in here and adjust the stats being created until it totaled something above the adjustment. It just felt weird…
What if we had a way to easily swap in implementations of the Billing::Usage? It would then allow me to concoct any combination of usage and mock the expected values sent to Stripe.
Setter Injection
In a few other articles, I’ve heard this termed “accessors as collaborators”. Whatever the name, it was surprising how such a little a change could produce so much flexibility in my tests. And with that additional flexibility came confidence because it allowed me to test the edge cases with minimal overhead.
A couple things changed:
usage_service was created to extract the code to calculate customer usage
The usage method now calls the last_30_days method on usage_service
This is interesting because you’ll notice now that the only important idea about usage_service is the fact that is has a last_30_days method. We can now take comfort in the idea that usage_service could be anything really, as long as it implements the last_30_days method.
attr_writer :usage_service was added to allow for other implementations of the usage class
This allows us to inject other forms of the usage_service to simulate more or less customer usage:
I’ve created classes for each usage tier that implement the last_30_days method. In real life, this usage service is more complex, but we can test the complexity of it alone through unit tests. The responsibility of this class is to ensure invoice items are added to Stripe correctly, so removing the complexity of Billing::Usage form this test allows us to maximize this test’s value and keep us isolated from the implementation of Billing::Usage — assuming it implements the last_30_days method.
Constructor Injection
Most dependency injection posts focus on constructor injection. The idea being that an implementation can be supplied. If not, a reasonable default will be provided. How might that change this scenario?
Because the usage method requires instantiation from within the class, I had to update the fake test Usage classes to accept user as an argument during instantiation:
The resulting test classes seem overly complex and sprinkled with details that aren’t particularly relevant to its responsibility. If we were to pass in an already instantiated usage class as an argument, it means we would have to already know the user before-hand, which means we’d have to parse @user ||= User.find_by(stripe_id: payload["data"]["object"]["customer”]) outside of this class. I don’t love that solution — the parent that calls this InvoiceCreated class is pretty minimal and I wanted to keep it that way.
Another option would be to provide user as an argument to the to the last_30_days method:
We could then change our fake test usage classes back to:
Notice the lack of Struct with an argument…
Summary
Of the two options, I prefer the setter injector in this case. There’s something about changing the signature of a class just for tests that didn’t feel natural.
An accessor (or writer…), in this case, provided the same flexibility without changing the signature. I like being able to quickly look at the argument list of initialize and clearly understand its roles and responsibilities within the system.