Daniel Hoelbling-Inzko talks about programming
I just ran into this and wanted to share this quite puzzling behavior:
20.times { Fabricate(:post) } Post.scoped.limit(10).length.should eq(10) # => Test failed # expected: 12 # got: 20
Weird right? But once you look at MongoDB's capabilities and the Mongoid implementation you'll quickly discover that this is only kind of a semi-bug :).
MongoDB supports an .count()
command that takes a filter and returns the number of matches documents. This command is being issued by Mongoid whenever you call .length
on a criterion (since it's much cheaper than executing the actual query and counting the returned objects).
So don't write tests like this:
# method under test: def self.most_played self.order_by(:play_count.desc).limit(12) end # test it 'returns the 12 most played items' do Video.most_played.length.should eq(12) end
This test will fail, even though the method is doing everything perfectly right. I'd even say that in a regular ActiveRecord setting this would be a passing test. But since I am utilizing the delayed execution functionality of Mongoid most_played
method is only returning a Criteria object that represents the query and can be composed into a more complicated query.
This causes .length
to be executed on Criteria where it causes Mongoid to call the .count()
method instead of running .length
on Array
. Since limit is no query parameter it will not be passed to MongoDB and the returning count will correctly be equal to the total number of Documents in the collection.
The more correct way to write that test is to actually force Mongoid to execute it's query and count the result:
it 'returns the 12 most played items' do Video.most_played.to_a.length.should eq(12) end