Ultimate rspec matcher to test named_scope or scoped

ADSENSE HERE!
After having a good practice on using Ultimate rspec matcher to test validation I think it's time to implement one for testing named scopes - custom finders. Testing these finders is daily task. Here is how it can be done with minimum amount of code and maximum readability.

Discovery (Animal Planet)



What do we expect from the custom finder?
We expect that it should find assets A, B, C and should not find assets D, E, F.
And sometimes the order is important: it should find A, B C with exact order.

With respect to let rspec feature let's take an example: Product has and belongs to many categories. We need to have a scope to filter products within the specified category:

describe "#by_category_id" do
let(:given_category) do
Factory.create(:given_category)
end


let(:product_in_given_category) do
Factory.create(
:product,
:categories => [category]
)
end

let(:product_not_in_given_category) do
Factory.create(
:product,
:categories => [Factory.create(:category)]
)
end

# This might be tricky to redefine subject as the finder result
# but in this way we can delegate the matcher to subject and
# avoid writing test descriptions.
subject { Product.by_category_id(given_category.id) }

it { should discover(product_in_given_category) }
it { should_not discover(product_not_in_given_category) }

end


Factory girl was used in this example because factories kickass when we test finders. As you can see the example has a perfect readability with no one line of plain English text. I didn't include the description in my examples but you can easily make them if they make sense for you.
Note: Be aware of the lazy loading of your finder. let is initialized lazy too. You should make sure it is called before the actual query to the database.
If you don't want to care about lazy loading their is let! method that could be easily copy-pasted from Rspec 2.0. Unlike let it doesn't have lazy initialization:
def let!(name, &block)
let(name, &block)
before { __send__(name) }
end


Testing sort order



If the ordering is done in non-trivial way let's discover.with_exact_order.
describe "#most_commented named scope" do
let(:uncommented_post) { Factory.create(:post)}
let!(:less_commented_post) { Factory.create(:post, :comments => [Factory.build(:comment)])}
let!(:more_commented_post) {
Factory.create(:post, :comments => [Factory.build(:comment), Factory.build(:comment)])}
}

subject { described_class.most_commented }
it {should discover(more_commented_post, less_commented_post).with_exact_order }
it {should_not discover(uncommented_post) }
end


Be careful with default order. MySQL and Postgres sort objects as they were created by default.
That is why generate objects in reverse order e.g. less_commented_post before more_commented_post is important to make sure that ordering is your code behavior rather than default db behavior.

Summary


I 've add this matcher to the previous one. Both matchers are available here accept_values_for. Let's start thinking of what else we can do.
ADSENSE HERE!

1 comment:

  1. I like accept_value_for. I don't know about discover.

    Why not do:

    subject { Post.approved }
    it { should include(expected_to_match) }
    it { should_not include(expected_not_to_match) }

    Or, for more control, I just check the arrays themselves:

    it { should == [ expected_to_match ] }

    This will let you check the order and the 'expected_not_to_match' object cannot be there.

    BTW: any ideas on supporting ActiveModel with AcceptValuesFor?

    ReplyDelete

Komen dong, tapi yang sopan dan tidak spam ya

Copyright © Spesial Unik. All rights reserved. Template by CB. Theme Framework: Responsive Design