Search code examples
ruby-on-railsrspecrspec-railsrspec3

Rspec passes class method but fails instance method


I'm trying to write a failing Rspec test. The actual test is associated with much longer code, but I narrowed down the problem to the class method it's testing.

Here's the test in Rspec:

context "For '.CASH.' as a stock" do
  let!(:cash) { FactoryGirl.create(:stock, symbol: '.CASH.', name: 'cash', status: 'Available') }

  describe "When update_stock runs on it" do
    it "should still have an 'Available' status" do
      # status should be 'Error' and test should fail
      Stock.change_to_error
      expect(cash.status).to eq('Available')
    end
  end
end

This is testing a model class method in Stock.rb:

def self.change_to_error
  self.all.each do |stock|
    stock.status = "Error"
    stock.save
  end
end

For some reason, this passes. However, if I changed it to use an instance method, it will fail like it should:

If stock_spec.rb changed to instance method:

context "For '.CASH.' as a stock" do
let!(:cash) { FactoryGirl.create(:stock, symbol: '.CASH.', name: 'cash', status: 'Available') }

  describe "When update_stock runs on it" do
    it "should still have an 'Available' status" do
        # status should be 'Error' and test should fail
        cash.change_to_error
        expect(cash.status).to eq('Available')
    end
  end
end

And if stock.rb class method turned into an instance method:

def change_to_error
  self.status = 'Error'
  self.save
end

This would pass. Unfortunately, I have to use a class method instead of an instance method because I want to update all stocks in the DB. "Change_to_error" methods are just there to figure out the problem. Does anyone know why it passes as a class method when it should fail? But it fails correctly when it's using an instance method?

Effectively, what is happening is that the class method does not change the status attribute of 'cash', but the instance method does. I don't know why that is happening.

FYI, I'm using rspec-rails


Solution

  • Solution: Need to put 'cash.reload' after 'Stock.change_to_error' and before the expect line.

    When using let! the object is created before the test. Updating the underlying data outside the object causes the instance to be outdated. Calling reload on it forces ActiveRecord to refresh it from the database.


    When you use let, RSpec does not call the block until the first time you reference the attribute, in this case, cash. So in your first example, you're running change_to_error on no records at all and then checking the status on cash, a record that gets created on the line with expect. In your second example, the cash object is created, then changed to an error. I'd recommend tailing your log to confirm this (tail -f log/test.log)

    If you change to let!, RSpec will create the object before every example is run. Another alternative is to reference cash in your example before calling change_to_error on all records that are created.