15

I am trying to test my default scope line in a model.

My test is as follows:

it 'orders by ascending name by default' do
  expect(Coaster.scoped.to_sql).to eq Coaster.order(:name).to_sql
end

My error is:

expected: "SELECT \"coasters\".* FROM \"coasters\"  ORDER BY name ASC, name"
got: "SELECT \"coasters\".* FROM \"coasters\"  ORDER BY name ASC"

What does the , name part at the end of the first line of the error mean and how can I resolve this?

UPDATE:

My test:

  describe 'default scope' do
    let!(:coaster_one) { FactoryGirl.create(:coaster, name: "Tower of Terror") }
    let!(:coaster_two) { FactoryGirl.create(:coaster, name: "Apocalypse") }

    it 'orders by ascending name' do
      Coaster.all.should eq [:coaster_two, :coaster_one]
    end
  end

My errors:

expected: [:coaster_two, :coaster_one]
            got: [#<Coaster id: 5, name: "Apocalypse", height: "60", speed: 60.0, length: "160", inversions: 4, material: nil, notes: nil, lat: nil, lng: nil, manufacturer_id: nil, park_id: 408, created_at: "2013-07-23 20:48:52", updated_at: "2013-07-23 20:48:52", slug: "apocalypse-at-alton-towers", style: nil, covering: nil, ride_style: nil, model: nil, layout: nil, order: nil, dates_ridden: nil, times_ridden: nil>, #<Coaster id: 4, name: "Tower of Terror", height: "60", speed: 60.0, length: "160", inversions: 4, material: nil, notes: nil, lat: nil, lng: nil, manufacturer_id: nil, park_id: 407, created_at: "2013-07-23 20:48:52", updated_at: "2013-07-23 20:48:52", slug: "tower-of-terror-at-alton-towers", style: nil, covering: nil, ride_style: nil, model: nil, layout: nil, order: nil, dates_ridden: nil, times_ridden: nil>]

       (compared using ==)

UPDATE 2:

It looks as though Rails 4 deprecates the use of default_scope so in light of this, I have remove the default_scope from my model and replaced it with a standard scope.

The new scope is now:

scope :by_name_asc, lambda { order("name ASC") }

and my associated test is:

  describe 'scopes' do
    let!(:coaster_one) { FactoryGirl.create(:coaster, name: "Tower of Terror") }
    let!(:coaster_two) { FactoryGirl.create(:coaster, name: "Apocalypse") }

    it "orders coasters by ascending name" do
      Coaster.by_name_asc.should eq [:coaster_two, :coaster_one]
    end
  end

When running this test I get:

  1) Coaster scopes orders coasters by ascending name
     Failure/Error: Coaster.by_name_asc.should eq [:coaster_two, :coaster_one]

       expected: [:coaster_two, :coaster_one]
            got: [#<Coaster id: 15, name: "Apocalypse", height: "60", speed: 60.0, length: "160", inversions: 4, material: nil, notes: nil, lat: nil, lng: nil, manufacturer_id: nil, park_id: 528, created_at: "2013-07-24 22:36:50", updated_at: "2013-07-24 22:36:50", slug: "apocalypse-at-alton-towers", style: nil, covering: nil, ride_style: nil, model: nil, layout: nil, order: nil, dates_ridden: nil, times_ridden: nil>, #<Coaster id: 14, name: "Tower of Terror", height: "60", speed: 60.0, length: "160", inversions: 4, material: nil, notes: nil, lat: nil, lng: nil, manufacturer_id: nil, park_id: 527, created_at: "2013-07-24 22:36:50", updated_at: "2013-07-24 22:36:50", slug: "tower-of-terror-at-alton-towers", style: nil, covering: nil, ride_style: nil, model: nil, layout: nil, order: nil, dates_ridden: nil, times_ridden: nil>]

       (compared using ==)

       Diff:
       @@ -1,2 +1,2 @@
       -[:coaster_two, :coaster_one]
       +[#<Coaster id: 15, name: "Apocalypse", height: "60", speed: 60.0, length: "160", inversions: 4, material: nil, notes: nil, lat: nil, lng: nil, manufacturer_id: nil, park_id: 528, created_at: "2013-07-24 22:36:50", updated_at: "2013-07-24 22:36:50", slug: "apocalypse-at-alton-towers", style: nil, covering: nil, ride_style: nil, model: nil, layout: nil, order: nil, dates_ridden: nil, times_ridden: nil>, #<Coaster id: 14, name: "Tower of Terror", height: "60", speed: 60.0, length: "160", inversions: 4, material: nil, notes: nil, lat: nil, lng: nil, manufacturer_id: nil, park_id: 527, created_at: "2013-07-24 22:36:50", updated_at: "2013-07-24 22:36:50", slug: "tower-of-terror-at-alton-towers", style: nil, covering: nil, ride_style: nil, model: nil, layout: nil, order: nil, dates_ridden: nil, times_ridden: nil>]

     # ./spec/models/coaster_spec.rb:10:in `block (3 levels) in <top (required)>'

Any ideas on what is going wrong?

rctneil
  • 7,016
  • 10
  • 40
  • 83
  • possible duplicate of [How can i have rspec test for my default scope](http://stackoverflow.com/questions/6853744/how-can-i-have-rspec-test-for-my-default-scope) – Joshua Pinter Dec 19 '14 at 19:21

6 Answers6

20

A better way to test your default scope is to use real data with an expected output. Create dummy objects for your class, then query the class and compare the result to what you expect to be the correct order:

describe 'default scope' do
  let!(:book_one) { Book.create(name: "The Count of Monte Cristo") }
  let!(:book_two) { Book.create(name: "Animal Farm") }

  it 'orders by ascending name' do
    Book.all.should eq [book_two, book_one]
  end
end

let!(:book_one) creates an instance of Book and assigns it to a local variable called book_one. Its name attribute is The Count of Monte Cristo. let!(:book_two) does something similar.

If the default order is name ASC, then querying the Book model should return an ActiveRecord relation with book_two as the first element ("Animal..."), and book_one as the second element ("The C...").

We can test this expectation as follows:

Book.all.should eq [book_two, book_one]

This does not test your SQL, but it tests the direct output of your code, which is more useful. It is also database independent.

Mohamad
  • 34,731
  • 32
  • 140
  • 219
  • Please could you explain your code a bit more as i'm a bit confused by it. What does let! do? – rctneil Jul 23 '13 at 17:35
  • Ah right, ok, so the let! lines: Are they the same as saying: :book_one = Book.create(....) ? – rctneil Jul 23 '13 at 17:45
  • The `let!` lines prime your database and create local variables you can use later to construct your expectation: `[:book_two, :book_one]` – Mohamad Jul 23 '13 at 17:47
  • Are you actually using a default scope? When I wrote that, I just wrote `Model.all`. It's better write a custom scope. Default scopes are generally not recommended and they can a pain in the neck. Try: `scope :by_name_desc -> { order(name: :desc) }` -- if you are not using Rails 4 then `order('model.name desc')` – Mohamad Jul 23 '13 at 21:32
  • Please could you check Update 2 in my OP? – rctneil Jul 24 '13 at 22:40
  • You're comparing an array of ActiveRecord objects to an array of symbols. You need to pass in a variable you created using `let!`: `[coaster_one, coaster_two]` not `[:coaster_one, :coaster_two]` notice the colons. – Mohamad Jul 25 '13 at 03:22
  • @rctneil oopps, my bad. I made the mistake, not you. My brain slipped. I updated the code. – Mohamad Jul 25 '13 at 15:51
  • No worries at all. We al make mistakes. Why does the array at the end need non symbols when you specify them using let! as symbols? Doesn;t sound right to me? I guess there is a reason? – rctneil Jul 25 '13 at 20:03
  • The symbol you pass to `let!` is a placeholder for the name of the variable it creates. You're telling `let!` to create a variable called `coaster_one` by passing in a symbol called `:coaster_one`. It's the same as `coaster_one = Factory.create(:coaster)`. You could pass in a string, I think. When you make the comparison, though, you want to be sure the array of objects coming back from `Foo.all` is the same as the array of objects you expect. So you construct one based on your expectation. – Mohamad Jul 25 '13 at 20:56
  • Brilliant. Thankyou so much for your help. I'm sure i'll be posting more rSpec questions over the next weeks! But thanks again. – rctneil Jul 25 '13 at 21:53
3

Your example is adding a second order term to the default, so you're getting name ASC, name. Try stripping off the first order clause first with reorder(''), e.g.:

expect(Coaster.scoped.to_sql).to eq Coaster.reorder('').order('name ASC').to_sql
Mori
  • 27,279
  • 10
  • 68
  • 73
3

DRY It Up, Use Shared Examples

Most likely, you'll have more than one model with a similar default scope (if not, mostly ignore this answer) so you can put this Rspec example into a shared_example where you can call it from a variety of model specs.

My preferred method of checking a default scope is to make sure the default ActiveRecord::Relation has the expected clause (order or where or whatever the case may be), like so:

spec/support/shared_examples/default_scope_examples.rb

shared_examples_for 'a default scope ordered by name' do

  it 'adds an ordered by name clause' do
    described_class.scoped.order_clauses.should include("name")
  end

end

And then in your Coaster spec (and any other spec that has the same default scope), all you need to is:

spec/models/coaster_spec.rb

describe Coaster

  it_behaves_like 'a default scope ordered by name'

  # other spec examples #

end

Finally, you can see how this pattern can be extended to all sorts of default scopes and keeps things clean, organized and, best of all, DRY.

Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
0

about the last update, it should be:

Coaster.all.should eq [coaster_two, coaster_one]

instead of:

Coaster.all.should eq [:coaster_two, :coaster_one]

Beelphegor
  • 226
  • 1
  • 12
0

We often try to test scopes with factories and real values. When we just want to confirm that the scope is setup correctly, we do the following:

  describe '.sorted' do
    let(:nodes) { described_class.sorted.order_values }
    let(:columns) { nodes.map { |n| n.instance_variable_get('@expr').name } }

    it do
      expect(columns)
        .to eq [:service_type, :mri_type, :ct_slices, :mri_magnet_strength,
                :created_at]
    end

    it { expect(nodes.map(&:direction)).to eq [:asc, :asc, :asc, :asc, :asc] }
  end

This is a test for:

  scope :sorted, (
    lambda do
      order(:service_type, :mri_type, :ct_slices, :mri_magnet_strength,
            :created_at)
    end
  )
Dan Kohn
  • 33,811
  • 9
  • 84
  • 100
0

You may get some deprecation warnings about Mohamad's answer, to resolve, just use expect and to

it 'orders by ascending name' do
  expect(Book.all).to eq [book_two, book_one]
end
JK Gunnink
  • 106
  • 6