factory | Dmytro Shteflyuk's Home https://kpumuk.info In my blog I'll try to describe about interesting technologies, my discovery in IT and some useful things about programming. Wed, 16 Dec 2009 22:11:12 +0000 en-US hourly 1 https://wordpress.org/?v=6.7.1 My top 7 RSpec best practices https://kpumuk.info/ruby-on-rails/my-top-7-rspec-best-practices/ https://kpumuk.info/ruby-on-rails/my-top-7-rspec-best-practices/#comments Wed, 25 Nov 2009 16:22:31 +0000 http://kpumuk.info/?p=1080 I use RSpec in all my projects. It’s really hard to overemphasize how helpful it is and how much easier becomes your life if you have good specs coverage. But its outstanding flexibility enables many ways to make your specs awful: horribly slow, over-bloated, even non-readable sometimes. I do not want to teach you BDD […]

The post My top 7 RSpec best practices first appeared on Dmytro Shteflyuk's Home.]]>
I use RSpec in all my projects. It’s really hard to overemphasize how helpful it is and how much easier becomes your life if you have good specs coverage. But its outstanding flexibility enables many ways to make your specs awful: horribly slow, over-bloated, even non-readable sometimes. I do not want to teach you BDD and RSpec here, but instead I will give you some ideas how to improve your specs quality and increase efficiency of your BDD workflow.

1. Use before :all block carefully

Sometimes it looks like a good idea to create a test data in before :all block. But be careful — these blocks are not wrapped in a transaction, so the data will not be rolled back after the test. In this case you should clear your data in the after :all block manually.

1
2
3
4
5
6
7
8
9
10
11
12
13
describe Friendship do
  before :all do
    @users = (1..5).collect { Factory(:user) }
  end

  after :all do
    @users.each { |user| user.destroy! }
  end

  it 'should do something' do
    # Something interesting with @users
  end
end

Another option is to move your before :all blocks to before :each to make them rolled back automatically.

2. For each test create exactly what it needs

Fixtures are cool when you start working on a project. But they quickly become painful while project grows: you add a new field to a fixture and break a half of your tests. There are tons of plugins which could simplify test data creation, I personally recommend factory_girl: it’s pretty slick and easy to use.

1
2
3
4
5
6
7
8
Factory.define :user do |f|
  f.sequence(:login) { |n| "user#{n}" }
  f.email { |a| "#{a.login}@example.com" }
  f.description "Ruby on Rails Developer"
end

# Somewhere in specs
@user = Factory(:user, :admin => true)

3. Do not create hundreds of records for a particular spec

Sometimes you want to test a method which operates on large set of records (filtering, trimming, etc). For example, this method returns 50 most popular videos (and no more). The straight approach is to create 51 record and make sure, that the size of the returned array is 50. When I saw a code snippet like this in our project first time, I was surprised. There was a few more pieces sharing this behavior, so here is my advice: add a parameter to the method, which will limit the number of records to return. In this case you can create 3 records, and pass 2 as a parameter.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
describe User do
  it 'should return top users in User.top method' do
    @users = (1..3).collect { Factory(:user) }
    top_users = User.top(2).all
    top_users.should have(2).entries
  end
end

class User < ActiveRecord::Base
  # Select N top users. Returns 10 entries when called without arguments.
  #   User.top.all.size    # => 10
  #   User.top(2).all.size # => 2
  #
  named_scope :top, lambda { |*args| { :limit => (args.size > 0 ? args[0] : 10) } }
end

4. Do not over-mock

Mocking is interesting and sometimes very useful technology. You may mock just everything so you spec will not hit the database. But there is a catch: your model code may be changed some day causing callers to break. Since you mock everything, you will never get failing specs. So now you should update all your mocks to fit a new interface. Also you would not be able to find SQL queries errors if you have mocked them. Instead of this I use integration approach: controller should talk to models, which have to hit the database. Real database with real data (OK, not so real). The practice 2 can help you in test data creation.

Bad:

1
2
3
4
5
6
7
8
9
10
11
12
describe VideosController do
  describe '.create action' do
    it 'should assign top videos' do
      params = { :title => 'new video', :description => 'video description' }
      @video = mock_model(Video)
      Video.should_receive(:new).and_return(@video)
      @video.should_receive(:update_attributes).with(params).and_return(true)
      get :index, :video => params
      assigns[:video].should be(@video)
    end
  end
end

Good:

1
2
3
4
5
6
7
8
9
10
11
describe VideosController do
  describe '.create action' do
    it 'should assign top videos' do
      params = { :title => 'new video', :description => 'video description' }
      get :index, :video => params
      assigns[:video].should_not be_new_record
      assigns[:video].title.should == params[:title]
      assigns[:video].description.should == params[:description]
    end
  end
end

But you can use mocks to skip records retrieving from the database (make sure you have specs covering corresponding model code). Let me explain this. For example, you need to render 20 entries in an RSS feed. You could create 21 record in the database using a factory, and then ensure only 20 of them were retrieved, or you could mock your finder method and check its parameter. You may not like magic numbers like 20 in this particular case, and this is a good point. Just move this magic number to the config and ensure it was used to do the retrieval.

1
2
3
4
5
6
7
8
9
10
describe VideosController do
  describe '.index action' do
    it 'should assign top videos' do
      @videos = [mock_model(Video), mock_model(Video)]
      Video.should_receive(:top).with(50).and_return(@videos)
      get :index
      assigns[:top_videos].should be(@videos)
    end
  end
end

5. Use contexts

RSpec spec specifies how particular code should work. Usually, in the beginning you tell what you are going to describe in this spec, and inside describe block you specify what the code should do:

1
2
3
4
5
describe Video do
  it 'should return 5 records in Video.top method' do
    Video.top.should have(5).items
  end
end

Usually you have more than one it block for each method. To group related specs I recommend to use nested describe blocks. Since describe is aliased to context when placed inside another describe, I think it’s a good idea to use it for specs grouping. Each context may have its own before and after blocks (in this case parent blocks will be called right before child ones).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
describe Video do
  describe '.top' do
    it 'should return 5 records' do
      Video.top.should have(5).items
    end
  end

  context 'when just created' do
    before :each do
      @video = Video.new
    end
   
    # ...
  end
end

6. Create several test suites to speed up your workflow

There are many things you can do to make your BDD more efficient. We will take a look at two of them: creating a separate test suites and running recently modified specs.

There are several standard test suites configured in RSpec by default: spec:controllers, spec:views, spec:helpers, spec:lib. Check the rake -T spec output to get a list of available RSpec tasks. Let’s create a simple Rake tasks generator for spec suites:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SPEC_SUITES = [
  { :id => :acl, :title => 'access control', :files => %w(spec/controllers/**/acl_spec.rb) },
  { :id => :amazon, :title => 'Amazon libraries', :dirs => %w(spec/lib/amazon) }
]

namespace :spec do
  namespace :suite do
    SPEC_SUITES.each do |suite|
      desc "Run all specs in #{suite[:title]} spec suite"
      Spec::Rake::SpecTask.new(suite[:id]) do |t|
        spec_files = []
        if suite[:files]
          suite[:files].each { |glob| spec_files += Dir[glob] }
        end

        if suite[:dirs]
          suite[:dirs].each { |glob| spec_files += Dir["#{glob}/**/*_spec.rb"] }
        end

        t.spec_opts = ['--options', ""#{Rails.root}/spec/spec.opts""]
        t.spec_files = spec_files
      end
    end
  end
end

Check what tasks are available now:

1
2
3
4
5
~/test$ rake -T spec:suite

(in /Users/kpumuk/test)
rake spec:suite:acl     # Run all specs in access control spec suite
rake spec:suite:amazon  # Run all specs in Amazon libraries spec suite

It was easy! And now let’s take a look at the Rake task for running the recently touched specs (last 10 minutes).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Grab recently touched specs
def recent_specs(touched_since)
  recent_specs = Dir['app/**/*'].map do |path|
    if File.mtime(path) > touched_since
      spec = File.join('spec', File.dirname(path).split("/")[1..-1].join('/'),
      "#{File.basename(path, ".*")}_spec.rb")
      spec if File.exists?(spec)
    end
  end.compact

  recent_specs += Dir['spec/**/*_spec.rb'].select do |path|
    File.mtime(path) > touched_since
  end.uniq
end

namespace :spec do
  desc 'Run all recent specs in spec directory touched in last 10 minutes'
  Spec::Rake::SpecTask.new(:recent) do |t|
    t.spec_opts = ['--options', ""#{RAILS_ROOT}/spec/spec.opts""]
    t.spec_files = recent_specs(Time.now - 10.minutes)
  end
end

And don’t forget to check autospec and watchr gems.

7. Stop spec_helper from being loaded multiple times

Just don’t do that. If you got a big project, there is a chance that the spec_helper will be required in a many different ways: File.expand_path, File.join, etc.,— which results in it being loaded several times and it slows down your test suite!

To avoid this, add the following code at the top of your spec_helper.rb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# figure out where we are being loaded from
if $LOADED_FEATURES.grep(/spec\/spec_helper\.rb/).any?
  begin
    raise "foo"
  rescue => e
    puts <<-MSG
  ===================================================
  It looks like spec_helper.rb has been loaded
  multiple times. Normalize the require to:

    require "spec/spec_helper"

  Things like File.join and File.expand_path will
  cause it to be loaded multiple times.

  Loaded this time from:

    #{e.backtrace.join("\n    ")}
  ===================================================
    MSG
  end
end

It will show where you’ve tried to load spec_helper from so you will be able to fix it immediately. Also there is an interesting snippet of code here, which will find and replace all wrong includes.

Conclusion

RSpec is not a silver bullet. You can have 100% coverage and fine-grained specs, but it does not mean your application is completely bug-free. Refactor your specs, increase your programming level, and refactor again. Write specs for any issue that you, your QAs or users have faced. And remember: do not over-mock.

Credits

This is my first article written completely in Google Wave in collaboration with several good Russian rubyists. Thank you all, guys. I want to acknowledge the editorial help of Roman Dmytrenko and Alexey Kovyrin. Robby Russell created a great picture illustrating how sexy is RSpec. And thank you all my readers for your attention.

Did you like this article? You should follow me in Twitter here.

The post My top 7 RSpec best practices first appeared on Dmytro Shteflyuk's Home.]]>
https://kpumuk.info/ruby-on-rails/my-top-7-rspec-best-practices/feed/ 27