4
\$\begingroup\$

This is my first model RSpec test:

require 'spec_helper'

describe CashFlow do
  context 'DB Fields' do
    it { should have_db_column :amount_cents }
    it { should have_db_column :amount_currency }
    it { should have_db_column :user_id }

    it { should have_db_column :target_date }
    it { should have_db_column :created_at }
    it { should have_db_column :updated_at }
  end

  context 'Associations' do
    it { should belong_to :user }
    it { should have_and_belong_to_many :tags }
  end

  context 'Validation' do
    it { should validate_presence_of :amount_cents }
    it { should validate_numericality_of :amount_cents }
    it { should ensure_exclusion_of(:amount_cents).in_array([0,]) }

    it { should validate_presence_of :amount_currency }
    it { should validate_presence_of :target_date }

    it { should validate_presence_of :user }
  end

  context 'Scopes' do
    subject { CashFlow }

    context 'Incoming/Outcoming flows' do
      let(:incoming_cash_flows) { 4.times.map { create :cash_flow } }
      let(:outcoming_cash_flows) { 4.times.map { create :cash_flow, negative: true } }

      it 'should be able to return all and only incomings cash_flows' do
        expect(subject.incoming).to include *incoming_cash_flows
        expect(subject.incoming).not_to include *outcoming_cash_flows
      end

      it 'should be able to return all and only outcomings cash_flows' do
        expect(subject.outcoming).to include *outcoming_cash_flows
        expect(subject.outcoming).not_to include *incoming_cash_flows
      end
    end

    context 'Past/Future flows' do
      [-1, nil, 1].each do |num|
        _threshold = num ? Date.today + num.year : nil

        let(:threshold) { _threshold }
        let(:future_cash_flows) { 4.times.map { create :cash_flow, threshold: threshold } }
        let(:past_cash_flows) { 4.times.map { create :cash_flow, past: true, threshold: threshold } }

        it "should be able to return all and only past cash_flows with #{_threshold || 'current date (default)'} threshold" do
          expect(subject.past(threshold)).to include *past_cash_flows
          expect(subject.past(threshold)).not_to include *future_cash_flows
        end

        it "should be able to return all and only future cash_flows with #{_threshold || 'current date (default)'} threshold" do
          expect(subject.future(threshold)).to include *future_cash_flows
          expect(subject.future(threshold)).not_to include *past_cash_flows
        end
      end
    end
  end

  it 'should save specified currency' do
    %w(GEL RUB USD EUR).each do |currency|
      cash_flow_id = create(:cash_flow, currency: currency).id

      cash_flow = CashFlow.find(cash_flow_id)
      expect(cash_flow.amount.currency).to eq currency
    end
  end
end

Model:

class CashFlow < ActiveRecord::Base
  belongs_to :user
  has_and_belongs_to_many :tags

  monetize :amount_cents, with_model_currency: :amount_currency

  validates_presence_of :user, :target_date, :amount_cents, :amount_currency
  validates_exclusion_of :amount_cents, in: [0, ]

  scope :incoming, -> { where 'amount_cents > 0' }
  scope :outcoming, -> { where 'amount_cents < 0' }

  scope :past, lambda { |date = nil | where 'target_date < ?', date || Date.today}
  scope :future, lambda { |date = nil | where 'target_date > ?', date || Date.today}
end

Factory:

FactoryGirl.define do
  factory :cash_flow do
    association :user

    ignore do
      negative false
      past false
      threshold nil
      currency 'USD'
    end

    after :build do |cash_flow, evaluator|
      sign = evaluator.negative ? -1 : 1
      cash_flow.amount = Money.new((rand(10000) + 1) * sign, evaluator.currency || currency)

      sign = evaluator.past ? -1 : 1
      cash_flow.target_date = (evaluator.threshold || Date.today) + ((rand(60 * 24 * 30) + 1) * sign).minutes
    end
  end
end

What can you tell me about its readability, flexibility and coverage fullness?

I use factory girl, shoulda and money gems.

\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

First thing: congratulations on starting with rspec! Way to go.

My first remark would be to remove your three first contexts (Fields, Associations & Validations).

Why? Because those are not testing any parts of your code - they are testing ActiveModel functionalities. You want to test your code, not your librairies - at least, not without a very good reason to think that there is a problem there. This is time spent to write specs that do not provide any benefits.

On the other side, while your model don't have much logic yet, I like the fact that you tested your scopes - this is your logic, and it should be tested.

\$\endgroup\$
5
  • \$\begingroup\$ I would keep the validation tests. In terms of spec'ing a model, I'd say it's valid (no pun intended) to specify that it should perform certain validations. A model may well have an attribute that's not used anywhere in the business logic (and thus won't be tested indirectly), but its presence is required for external reasons. \$\endgroup\$
    – Flambino
    Commented Mar 21, 2014 at 13:31
  • \$\begingroup\$ I would test the validations - but not as "is there validations" but as "should refuse an amount without currency" with an appropriate fixture. Now agree this maybe a question of preferences. \$\endgroup\$
    – Martin
    Commented Mar 21, 2014 at 14:26
  • 1
    \$\begingroup\$ That's an even better way to go. But your answer didn't give an example of a spec that could replace or improve upon the shoulda tests (you did that in your comment just now, however), so it could be interpreted as simply "don't test validation" \$\endgroup\$
    – Flambino
    Commented Mar 21, 2014 at 14:51
  • \$\begingroup\$ You are right. I'll update my answer accordingly. Thanks! \$\endgroup\$
    – Martin
    Commented Mar 21, 2014 at 14:52
  • \$\begingroup\$ Thank you guys, I myself thought that DB fields testing is excessive, but as for validation, in terms of TDD I think it should be done, especially with help of shoulda matchers, because they check is there a particular validation or not. \$\endgroup\$
    – atomAltera
    Commented Mar 22, 2014 at 4:03
0
\$\begingroup\$

Sounds quite complete. I just do not like to have any logic in my tests that make it hard to debug, like you did when executed some test using array [-1, nil, 1] . Also make sense what first comment says, to do not test the framework, but do test your code.

\$\endgroup\$
2
  • \$\begingroup\$ I used loop (iterating through array) in place of 3 quite similar tests, is this bad approach? \$\endgroup\$
    – atomAltera
    Commented Mar 23, 2014 at 4:35
  • \$\begingroup\$ I woundn't say bad, but just more complext. I just like my tests as simple as possible, that's why I don't like to use. Some books says that every test case should have only one assert, so when you iterate in a array you are creating multiple asserts. Let suppose you are considering a range of [-1, 0, 1], and that represents a negative, neutral and positive account balance, so I think is more clear if you create three different tests. \$\endgroup\$
    – jlucasps
    Commented Mar 23, 2014 at 6:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.