How to recognize design cues by looking at the tests?

Objective


In this article, you will learn how to recognize design cues by looking at the tests so that you can improve your design.

Discussion


In an existing Rails project, I was creating objects like this in the tests:

Foo.new(x, nil, nil)

As you can see from the specs:

describe FraudPolicy do

  context 'Spoofed receiver_email' do
    before do
      order = double('Order') 
      order.stub_chain(:product, :user, :primary_paypal_email) { 'seller@rubyplus.com' }
      Order.stub(:find_by_number) { order }    
    end

    it 'should identify spoofed receiver email' do
      notify = stub('Notify', invoice: 10, account: 'spoofer@spooky.net')

      fraud_policy = FraudPolicy.new(notify, nil, nil)

      expect(fraud_policy.spoofed_receiver_email?).to be(true)
    end

    it 'should identify non spoofed receiver email' do
      notify = stub('Notify', invoice: 10, account: 'seller@rubyplus.com')

      fraud_policy = FraudPolicy.new(notify, nil, nil)

      expect(fraud_policy.spoofed_receiver_email?).to be(false)    
    end    
  end
end

In another context:

Foo.new(x, y, z)

I was using all the parameters. This means some of the tests does not need some attributes to exercise their functionality. This happens when the class operates on its attributes for some of the operations. Ideally we want our classes to have very high cohesion. This can be achieved by creating classes that operate on its data most of the time.

No Pain No Gain, Really?


The tests does not tell you explicitly that your design needs improvement. If there is no pain in testing, you usually move on to the next task. In this case, the test was easy to write and I moved on to other tasks without reflecting on the resulting design. The question is : How can we gain when there is no pain?

Small Cohesive Composable Objects


Legos

Later, I extracted bacardi gem from this project and improved the design. Let's use bacardi gem as an example for our discussion. After refactoring, the FraudCheck class now looks like this:

module Bacardi

  class FraudCheck
    def initialize(order, notifier)
      @order = order
      @notifier = notifier
    end

    def no_malicious_product_change?
       (@order.product_id == @notifier.item_number) && (@order.product_name == @notifier.item_name)
    end
  end

end

Both attributes are used by the only method in this class. It also resulted in lot of small classes that were split to increase the cohesion. I was able to compose the small objects in another class to implement the required functionality. Here is another class from that project:

require 'bigdecimal'

module Bacardi

  class Notification
    def initialize(notifier)
      @notifier = notifier
    end

    def paid_currency
      @notifier.currency
    end

    def paid_gross
      BigDecimal.new((@notifier.gross.to_f.abs.to_i) * 100)
    end

    def confirmation_number
      @notifier.invoice
    end

    def receiver_email
      @notifier.account
    end
  end

end

The notifier attribute is used by all the methods in this case. Here is another small class with very high cohesion:

require 'bigdecimal'

module Bacardi

  class Payment
    def initialize(transaction)
      @transaction = transaction
    end

    def actual_currency
      @transaction.currency
    end

    def actual_gross
      BigDecimal.new((@transaction.gross.to_f.abs.to_i) * 100)
    end
  end

end

Finally:

require 'money'

module Bacardi

  class ProductAmount
    def initialize(notification, payment)
      @notification = notification
      @payment = payment
    end

    def correct?
      actual_gross = @payment.actual_gross
      actual_currency = @payment.actual_currency
      paid_gross = @notification.paid_gross
      paid_currency = @notification.paid_currency

      actual = ::Money.new(actual_gross, actual_currency)
      paid = ::Money.new(paid_gross, paid_currency)
      actual == paid
    end
  end

end

This class is small and similar to other classes. The difficulty is in coming up with good names for the new classes. It forces you to find the missing abstraction. So you have to think hard to name them.

Summary


The symmetry in code results in beautiful code. If you observe that tests lack symmetry in the constructor you can think about increasing cohesion and decompose the bigger class into smaller cohesive units to improve the overall design. The resulting design uses composition to compose the smaller objects and implements the desired functionality using delegation.


Related Articles


Create your own user feedback survey