Benito Serna Tips and tools for Ruby on Rails developers

Which specs would you prefer for this presentation object?

February 17, 2021

I am currently working on a kind of “presentation object” that I called IdealPayments::GlobalStats.

This object will be used in two places, to display a graph and to show the data in a page where we can see all the “ideal payments”.

In my imagination and implementation, this object will depenend on an active record relation and chain the right methods to present the result.

Although I think this is not a big deal, I would like to know what do you think is best way to write unit/micro tests for this object and why?

I have two options…

  1. Test through the database as if the source (the active record relation) was invisible.
  2. Test by allowing that “the right messages” will be send to the “source” and expect the right result is returned.

Note: I already have test all the methods on the active record class through the database.

Option 1 - Test through the database as if the source (the active record relation) was invisible.

module IdealPayments
  RSpec.describe GlobalStats do
    describe "#generated_amount" do
      it "is the sum of the ideal payments amount" do
        create :ideal_payment, amount: 1_000
        create :ideal_payment, amount: 2_000
        expect(subject.generated_amount).to eq 3_000
      end

      it "is just for projects promoted by us" do
        dev_project = create :dev_project, promoter: "other"
        project = create :project, dev_project: dev_project
        ideal_payments_for_project = create :ideal_payments_for_project, project: project

        create :ideal_payment, ideal_payments_for_project: ideal_payments_for_project, amount: 1_000
        create :ideal_payment, amount: 2_000
        expect(subject.generated_amount).to eq 2_000
      end
    end

    describe "#paid_amount" do
      it "is the sum of the ideal payments paid amount" do
        create :ideal_payment, paid_amount: 1_000
        create :ideal_payment, paid_amount: 1_000
        expect(subject.paid_amount).to eq 2_000
      end
    end

    describe "#pending_amount" do
      it "is the difference between the generated amount and the paid amount" do
        create :ideal_payment, amount: 1_000, paid_amount: 500
        create :ideal_payment, amount: 1_000, paid_amount: 200
        expect(subject.pending_amount).to eq 2_000 - 700
      end
    end

    describe "#pending_amount_without_delay" do
      it "is the pending amount that is not yet delayed" do
        create :ideal_payment, paid_amount: 0, amount: 1500, date: Date.current + 10.days
        create :ideal_payment, paid_amount: 0, amount: 1000, date: Date.current - 10.days
        create :ideal_payment, paid_amount: 1500, amount: 2000, date: Date.current
        expect(subject.pending_amount_without_delay).to eq 2000
      end
    end

    describe "#delayed_amount" do
      it "is the pending amount that is delayed" do
        create :ideal_payment, paid_amount: 0, amount: 1500, date: Date.current
        create :ideal_payment, paid_amount: 0, amount: 1000, date: Date.current - 10.days
        create :ideal_payment, paid_amount: 1500, amount: 2000, date: Date.current - 5.days
        expect(subject.delayed_amount).to eq 1500
      end
    end

    describe "#delayed_amount_less_than_30_days" do
      it "is the pending amount that is delayed less than 30 days" do
        create :ideal_payment, paid_amount: 0, amount: 8000, date: Date.current + 10.days
        create :ideal_payment, paid_amount: 0, amount: 1000, date: Date.current - 30.days
        create :ideal_payment, paid_amount: 1500, amount: 2000, date: Date.current - 5.days
        expect(subject.delayed_amount_less_than_30_days).to eq 1500
      end
    end

    describe "#delayed_amount_between_30_and_90_days" do
      it "is the pending amount that is delayed more than 30 days but less than 90 days" do
        create :ideal_payment, paid_amount: 0, amount: 1500, date: Date.current - 90.days
        create :ideal_payment, paid_amount: 0, amount: 2500, date: Date.current - 30.days
        create :ideal_payment, paid_amount: 0, amount: 1000, date: Date.current - 50.days
        create :ideal_payment, paid_amount: 1500, amount: 2000, date: Date.current - 5.days
        expect(subject.delayed_amount_between_30_and_90_days).to eq 2500
      end
    end

    describe "#delayed_amount_more_90_days" do
      it "is the pending amount that is delayed more than than 90 days" do
        create :ideal_payment, paid_amount: 0, amount: 1500, date: Date.current - 100.days
        create :ideal_payment, paid_amount: 0, amount: 1000, date: Date.current - 90.days
        create :ideal_payment, paid_amount: 1500, amount: 2000, date: Date.current - 95.days
        expect(subject.delayed_amount_more_90_days).to eq 2000
      end
    end
  end
end

Option 2. Test by allowing that “the right messages” will be send to the “source” and expect the right result is returned.

module IdealPayments
  RSpec.describe GlobalStats do
    describe "#source" do
      it "is by default the ideal payments for projects promoted by us" do
        expected = double("Ideal payments for project promoted by us")
        allow(IdealPayment).to receive(:for_projects_promoted_by_us).and_return(expected)
        expect(subject.source).to eq expected
      end
    end

    describe "#generated_amount" do
      it "is the total amount" do
        allow(subject.source).to receive(:total_amount).and_return(3_000)
        expect(subject.generated_amount).to eq 3_000
      end
    end

    describe "#paid_amount" do
      it "is the total paid amount" do
        allow(subject.source).to receive(:total_paid_amount).and_return(2_000)
        expect(subject.paid_amount).to eq 2_000
      end
    end

    describe "#pending_amount" do
      it "is the total paid amount" do
        allow(subject.source).to receive(:total_pending_amount).and_return(1_000)
        expect(subject.pending_amount).to eq 1_000
      end
    end

    describe "#pending_amount_without_delay" do
      it "is the total pending amount of the not delayed payments" do
        allow(subject.source).to receive_message_chain(:not_delayed, :total_pending_amount).and_return(1_000)
        expect(subject.pending_amount_without_delay).to eq 1_000
      end
    end

    describe "#delayed_amount" do
      it "is the total pending amount of the delayed payments" do
        allow(subject.source).to receive_message_chain(:delayed, :total_pending_amount).and_return(1_000)
        expect(subject.delayed_amount).to eq 1_000
      end
    end

    describe "#delayed_amount_less_than_30_days" do
      it "is the total pending amount that is delayed less than 30 days" do
        delayed = double("Delayed")
        allow(subject.source).to receive(:delayed_less_than).with(30.days).and_return(delayed)
        allow(delayed).to receive(:total_pending_amount).and_return(1500)
        expect(subject.delayed_amount_less_than_30_days).to eq 1500
      end
    end

    describe "#delayed_amount_between_30_and_90_days" do
      it "is the total pending amount that is delayed more than 30 days but less than 90 days" do
        delayed = double("Delayed")
        allow(subject.source).to receive(:delayed_between).with(30.days, 90.days).and_return(delayed)
        allow(delayed).to receive(:total_pending_amount).and_return(2500)
        expect(subject.delayed_amount_between_30_and_90_days).to eq 2500
      end
    end

    describe "#delayed_amount_more_90_days" do
      it "is the pending amount that is delayed more than than 90 days" do
        delayed = double("Delayed")
        allow(subject.source).to receive(:delayed_more_than).with(90.days).and_return(delayed)
        allow(delayed).to receive(:total_pending_amount).and_return(3500)
        expect(subject.delayed_amount_more_90_days).to eq 3500
      end
    end
  end
end

Here is the actual implementation

module IdealPayments
  class GlobalStats
    def initialize(ideal_payments = IdealPayment.for_projects_promoted_by_us)
      @ideal_payments = ideal_payments
    end

    def source
      ideal_payments
    end

    def generated_amount
      ideal_payments.total_amount
    end

    def paid_amount
      ideal_payments.total_paid_amount
    end

    def pending_amount
      ideal_payments.total_pending_amount
    end

    def pending_amount_without_delay
      ideal_payments.not_delayed.total_pending_amount
    end

    def delayed_amount
      ideal_payments.delayed.total_pending_amount
    end

    def delayed_amount_less_than_30_days
      ideal_payments.delayed_less_than(30.days).total_pending_amount
    end

    def delayed_amount_between_30_and_90_days
      ideal_payments.delayed_between(30.days, 90.days).total_pending_amount
    end

    def delayed_amount_more_90_days
      ideal_payments.delayed_more_than(90.days).total_pending_amount
    end

    private

    attr_reader :ideal_payments
  end
end

Related articles

Weekly tips and tools for Ruby on Rails developers

I send an email each week, trying to share knowledge and fixes to common problems and struggles for ruby on rails developers, like How to fetch the latest-N-of-each record or How to test that an specific mail was sent or a Capybara cheatsheet. You can see more examples on Most recent posts or All post by topic.