Test critical paths in your app with ease thanks to Dependency Injection

Your ads will be inserted here by

Easy Plugin for AdSense.

Please go to the plugin admin page to
Paste your ad code OR
Suppress this ad slot.

Dependency Injection is one of my favorite programming patterns. In this short blogpost, I’ll present you how it helps testing potentially untestable code.

Imagine that your customer wants to easily identify orders in the e-commerce system which you are maintaining. They requested simple numeric identifier in a very specific 9-digit format which will make their life easier, especially when it comes to discussing order details with their client via the phone call. They want identifier starting with 100 and six random digits, e.g. 100123456.

Easy peasy you think, but you probably also know that the subset is limited to 999999 combinations and collisions may happen. You probably create a unique index on the database column, let’s call it order_number to prevent duplicates. However, instead of raising an error if the same number occurs again you want to make a retry.

Let’s start with a test for the best case scenario

 RSpec.describe OrderNumberGenerator do   specify do     order = Order.create!      OrderNumberGenerator.new.call(order.id)      expect(order.reload.order_number).to be_between(100_000_001, 100_999_999)   end end 

And the simple implementation:

Your ads will be inserted here by

Easy Plugin for AdSense.

Please go to the plugin admin page to
Paste your ad code OR
Suppress this ad slot.

 class OrderNumberGenerator   MAX_ATTEMPTS = 3    def initialize     @attempts = 0   end    def call(order_id)     order = Order.find(order_id)     order.order_number ||= random_number_generator.call     order.save!   rescue ActiveRecord::RecordNotUnique => doh      @attemps += 1      retry if @attemps < MAX_ATTEMPTS      raise doh   end    private    def random_number_generator     rand(100_000_001..100_999_999)   end end 

The code looks fine, but we’re not able to easily verify whether retry scenario works as intended. We could stub Ruby’s Kernel#rand but we want cleaner & more flexible solution, so let’s do a tiny refactoring.

 class RandomNumberGenerator   def call     rand(100_000_001..100_999_999)   end end  class OrderNumberGenerator   MAX_ATTEMPTS = 3    def initialize(random_number_generator: RandomNumberGenerator.new)     @attempts = 0     @random_number_generator = random_number_generator   end    def call(order_id)     order = Order.find(order_id)     order.order_number ||= @random_number_generator.call     order.save!   rescue ActiveRecord::RecordNotUnique => doh      @attemps += 1      retry if @attemps < MAX_ATTEMPTS      raise doh   end  end 

Random number generator is no longer a private method, but a separate class RandomNumberGenerator. It’s injected to OrderNumberGenerator and the code still works as before. Instead of a default RandomNumberGenerator, for the testing purposes we pass simple lambda. Lambda pops elements from crafted array to cause intended unique index violation.

 RSpec.describe OrderNumberGenerator do   specify do     order_1 = Order.create!     order_2 = Order.create!      numbers = [100_000_999, 100_000_001, 100_000_001, 100_000_001]     order_number_generator = OrderNumberGenerator.new(random_number_generator: -> { numbers.pop })      order_number_generator.call(order_1.id)      expect { order_number_generator.call(order_2.id) }.not_to raise_error   end    specify do     order_1 = Order.create!     order_2 = Order.create!      numbers = Array.new(4, 100_000_001)     order_number_generator = OrderNumberGenerator.new(random_number_generator: -> { numbers.pop })      order_number_generator.call(order_1.id)      expect { order_number_generator.call(order_2.id) }.to raise_error(ActiveRecord::RecordNotUnique)   end end 

Wrap up

As you can see, apart from being more confident about the critical code in our application due to having more test scenarios, we gained a lot of flexibility. Requirements related to order_number may change in the future. Injecting a different random_number_generator will do the job and core implementation of OrderNumberGenerator will remain untouched.

Leave a Reply

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