Writing tests is an inherent process of developing a good application. Sometimes code that works correctly, fails during tests. It’s not a bug in Rspec gem – in most cases it’s a test code error. As an example, I’m going to show you two errors from last few days, both referring to the same issue.

Example – subscriptions

Imagine an application where you can subscribe something, it doesn’t really matter what it is. You can suspend your subscription for a reason you choose. The application automatically changes its state the day the subscription ends. You’ll find a few methods inside the Subscription model. Generally, there are 5 methods, and two of them check the state and date of suspension. Another three methods are used for updates.

...
def suspended?
  state == 'suspended'
end

def suspension_passed?
  end_suspension_date < DateTime.now
end

def disable_suspension
  self.update_attributes(state: 'ready', start_suspension_date: nil, end_suspension_date: nil)
end

def unsuspend
  if self.suspended? && self.suspension_passed?
    disable_suspension
  end
end

def self.update_suspensions
  all.map(&:unsuspend)
end

Factories will help you create particular objects used for testing. I need three different factories for three situations. First one, when user subscription is ready (not suspended); second, when subscription is suspended; and the last one, when subscription is suspended, but should be updated to ready state.

FactoryGirl.define do
  factory :subscription, :class => 'Subscription' do

    factory :subscription_ready do
    state 'ready'
    end

    factory :suspended_subscription do
    state 'suspended'
      start_suspension_date DateTime.parse('01/01/2016')
      end_suspension_date DateTime.parse('02/01/2016')
    end

    factory :passed_subscription do
    state 'suspended'
      start_suspension_date DateTime.parse('01/01/2015')
      end_suspension_date DateTime.parse('02/01/2015')
    end
  end
end

Example 1 – model tests – fails

Let’s start with model tests. It is not a difficult task, just a couple of tests for each method. Not really exciting, but wait until you see the result.

describe 'suspension' do
  let!(:suspended_subscription) { FactoryGirl.create(:suspended_subscription) }
  let!(:ready_subscription) { FactoryGirl.create(:subscription_ready) }
  let!(:passed_subscription) { FactoryGirl.create(:passed_subscription) }

  describe 'suspended?' do
    it 'returns true if subscription in suspended' do
      expect(suspended_subscription.suspended?).to be true
    end

  it 'should set subscription to ready state' do
    suspended_subscription.disable_suspension
    expect(suspended_subscription.suspended?).to be false
    expect(suspended_subscription.start_suspension_date).to be nil
    expect(suspended_subscription.end_suspension_date).to be nil
  end

  describe 'passed?' do
    it 'returns true if subscription in suspended' do
      expect(passed_subscription.suspension_passed?).to be true
    end

    it 'returns false if subscription is ready' do
      expect(suspended_subscription.suspension_passed?).to be false
    end
  end

  it 'should unsuspend subscription' do
    passed_subscription.unsuspend
    expect(passed_subscription.suspended?).to be false
    expect(passed_subscription.start_suspension_date).to be nil
    expect(passed_subscription.end_suspension_date).to be nil
  end


  it 'should update subscriptions' do
    Subscription::Subscription.all.map(&:unsuspend)
    expect(passed_subscription.suspended?).to be false
    expect(passed_subscription.start_suspension_date).to be nil
    expect(passed_subscription.end_suspension_date).to be nil
    expect(ready_subscription.suspended?).to be false
    expect(suspended_subscription.suspended?).to be true
  end
end

What is the result?

Failures:
1) Subscription::Subscription suspension should update subscriptions
  Failure/Error: expect(passed_subscription.suspended?).to be false
    expected false
    got true

Example 1 – model tests – passed

The question is: why the last test failed? Why the last one only? It is a class-testing method that maps all subscription objects, and runs an unsuspend method on each object. The method was tested above, so why it’s not working? The difference between those functions is that the first one is working on the given object, while the second one uses the class. In should update subscriptions test, all objects are updated, but passed_subscription argument, checked in the next line, is still an object created from Factory. It’s not so easy to see at first – it took me quite a lot of time to realize what happened. The solution is presented below:

...
it 'should update subscriptions' do
  Subscription::Subscription.all.map(&:unsuspend)
  passed_subscription = Subscription::Subscription.all[2]
  ready_subscription = Subscription::Subscription.all[1]
  suspended_subscription = Subscription::Subscription.all[0]
  expect(passed_subscription.suspended?).to be false
  expect(passed_subscription.start_suspension_date).to be nil
  expect(passed_subscription.end_suspension_date).to be nil
  expect(ready_subscription.suspended?).to be false
  expect(suspended_subscription.suspended?).to be true
end

Now, objects are assigned to variables after the mapping, so they are reloaded, and updated with unsuspend method. And what is the result now?

Finished in 8.72 seconds (files took 4.31 seconds to load)
6 examples, 0 failures

Example 2 – feature tests – fails

Now, look at a similar situation with the feature test. User suspended his subscription, but he wants to cancel it. He clicks the ready button and the object is updated. Everything is working perfectly in development mode, but what about tests?

require 'rails_helper'
require 'i18n'
require 'support/login_helpers'

feature 'User subscriptions' do
  include LoginHelpers
  scenario 'user turns off the suspension' do
    user = FactoryGirl.create(:user)
    user_login user
    user.subscription = FactoryGirl.create(:suspended_subscription)
    expect(user.subscription.start_suspension_date).to eq(DateTime.parse('10/03/2015'))
    expect(user.subscription.end_suspension_date).to eq(DateTime.parse('12/03/2015'))
    visit '/subscriptions'
    click_button I18n.t 'user.subscriptions.state.ready'

    expect(page).to have_button I18n.t 'user.subscriptions.state.suspend'
    expect(user.subscription.state).to eq('ready')
    expect(user.subscription.start_suspension_date).to eq(nil)
    expect(user.subscription.end_suspension_date).to eq(nil)
  end
end

Again, you can predict that test will fail, but why?

Failures:
1) Subscription::User subscriptions
  Failure/Error: expect(user.subscription.state).to eq('ready')
    expected 'ready'
    got 'suspended'

Example 2 – feature tests – passed

It’s the same problem as in the previous example. Creating subscription works like assigning to user.subscription variable. User executes disable_suspension method after the button click. It changes the state of the user subscription, but old values are still assigned to the variable, which is then passed to expect method. All you need is one line with user.subscription.reload.

...
scenario 'user turns off the suspension' do
  user = FactoryGirl.create(:user)
  user_login user
  user.subscription = FactoryGirl.create(:suspended_subscription)
  expect(user.subscription.start_suspension_date).to eq(DateTime.parse('10/03/2015'))
  expect(user.subscription.end_suspension_date).to eq(DateTime.parse('12/03/2015'))
  visit '/subscriptions'
  click_button I18n.t 'user.subscriptions.state.ready'

  expect(page).to have_button I18n.t 'user.subscriptions.state.suspend'
  user.subscription.reload
  expect(user.subscription.state).to eq('ready')
  expect(user.subscription.start_suspension_date).to eq(nil)
  expect(user.subscription.end_suspension_date).to eq(nil)
end

The expect method updates user.subscription, so now it has correct state and dates. The test passes now.

inished in 8.72 seconds (files took 4.31 seconds to load)
7 examples, 0 failures

Conclusions

I wanted to show you two of the mistakes I made while writing the test code. It took me some time to understand what really happened there. Apparently, writing Rspec code exactly the same way as in the console isn’t the best approach. Each time an object created from the factory is assigned to variable, the update method has no influence on the latter. I believe these are very specific bugs, quite hard to examine. Broken code is more ‘console’ approach. Each time I got an initial idea on how it should work, surprisingly, it didn’t. I hope this article will help you in analyzing failed tests.

Post tags:

Join our awesome team
Check offers

Work
with us

Tell us about your idea
and we will find a way
to make it happen.

Get estimate