Testing ActiveRecord Pagination Integration in will_paginate
This test suite validates the integration of will_paginate with ActiveRecord, focusing on pagination functionality across various ActiveRecord operations and associations. The tests ensure proper behavior of pagination methods, counting mechanisms, and query optimizations.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
mislav/will_paginate
spec/finders/active_record_spec.rb
require 'spec_helper'
require 'will_paginate/active_record'
require File.expand_path('../activerecord_test_connector', __FILE__)
ActiverecordTestConnector.setup
RSpec.describe WillPaginate::ActiveRecord do
extend ActiverecordTestConnector::FixtureSetup
fixtures :topics, :replies, :users, :projects, :developers_projects
it "should integrate with ActiveRecord::Base" do
expect(ActiveRecord::Base).to respond_to(:paginate)
end
it "should paginate" do
expect {
users = User.paginate(:page => 1, :per_page => 5).to_a
expect(users.length).to eq(5)
}.to execute(2).queries
end
it "should fail when encountering unknown params" do
expect {
User.paginate :foo => 'bar', :page => 1, :per_page => 4
}.to raise_error(ArgumentError)
end
describe "relation" do
it "should return a relation" do
rel = nil
expect {
rel = Developer.paginate(:page => 1)
expect(rel.per_page).to eq(10)
expect(rel.current_page).to eq(1)
}.to execute(0).queries
expect {
expect(rel.total_pages).to eq(2)
}.to execute(1).queries
end
it "should keep per-class per_page number" do
rel = Developer.order('id').paginate(:page => 1)
expect(rel.per_page).to eq(10)
end
it "should be able to change per_page number" do
rel = Developer.order('id').paginate(:page => 1).limit(5)
expect(rel.per_page).to eq(5)
end
it "remembers pagination in sub-relations" do
rel = Topic.paginate(:page => 2, :per_page => 3)
expect {
expect(rel.total_entries).to eq(4)
}.to execute(1).queries
rel = rel.mentions_activerecord
expect(rel.current_page).to eq(2)
expect(rel.per_page).to eq(3)
expect {
expect(rel.total_entries).to eq(1)
}.to execute(1).queries
end
it "supports the page() method" do
rel = Developer.page('1').order('id')
expect(rel.current_page).to eq(1)
expect(rel.per_page).to eq(10)
expect(rel.offset).to eq(0)
rel = rel.limit(5).page(2)
expect(rel.per_page).to eq(5)
expect(rel.offset).to eq(5)
end
it "raises on invalid page number" do
expect {
Developer.page('foo')
}.to raise_error(ArgumentError)
end
it "supports first limit() then page()" do
rel = Developer.limit(3).page(3)
expect(rel.offset).to eq(6)
end
it "supports first page() then limit()" do
rel = Developer.page(3).limit(3)
expect(rel.offset).to eq(6)
end
it "supports #first" do
rel = Developer.order('id').page(2).per_page(4)
expect(rel.first).to eq(users(:dev_5))
expect(rel.first(2)).to eq(users(:dev_5, :dev_6))
end
it "supports #last" do
rel = Developer.order('id').page(2).per_page(4)
expect(rel.last).to eq(users(:dev_8))
expect(rel.last(2)).to eq(users(:dev_7, :dev_8))
expect(rel.page(3).last).to eq(users(:poor_jamis))
end
end
describe "counting" do
it "should guess the total count" do
expect {
topics = Topic.paginate :page => 2, :per_page => 3
expect(topics.total_entries).to eq(4)
}.to execute(1).queries
end
it "should guess that there are no records" do
expect {
topics = Topic.where(:project_id => 999).paginate :page => 1, :per_page => 3
expect(topics.total_entries).to eq(0)
}.to execute(1).queries
end
it "forgets count in sub-relations" do
expect {
topics = Topic.paginate :page => 1, :per_page => 3
expect(topics.total_entries).to eq(4)
expect(topics.where('1 = 1').total_entries).to eq(4)
}.to execute(2).queries
end
it "supports empty? method" do
topics = Topic.paginate :page => 1, :per_page => 3
expect {
expect(topics).not_to be_empty
}.to execute(1).queries
end
it "support empty? for grouped queries" do
topics = Topic.group(:project_id).paginate :page => 1, :per_page => 3
expect {
expect(topics).not_to be_empty
}.to execute(1).queries
end
it "supports `size` for grouped queries" do
topics = Topic.group(:project_id).paginate :page => 1, :per_page => 3
expect {
expect(topics.size).to eq({nil=>2, 1=>2})
}.to execute(1).queries
end
it "overrides total_entries count with a fixed value" do
expect {
topics = Topic.paginate :page => 1, :per_page => 3, :total_entries => 999
expect(topics.total_entries).to eq(999)
# value is kept even in sub-relations
expect(topics.where('1 = 1').total_entries).to eq(999)
}.to execute(0).queries
end
it "supports a non-int for total_entries" do
topics = Topic.paginate :page => 1, :per_page => 3, :total_entries => "999"
expect(topics.total_entries).to eq(999)
end
it "overrides empty? count call with a total_entries fixed value" do
expect {
topics = Topic.paginate :page => 1, :per_page => 3, :total_entries => 999
expect(topics).not_to be_empty
}.to execute(0).queries
end
it "removes :include for count" do
expect {
developers = Developer.paginate(:page => 1, :per_page => 1).includes(:projects)
expect(developers.total_entries).to eq(11)
expect($query_sql.last).not_to match(/\bJOIN\b/)
}.to execute(1).queries
end
it "keeps :include for count when they are referenced in :conditions" do
developers = Developer.paginate(:page => 1, :per_page => 1).includes(:projects)
with_condition = developers.where('projects.id > 1')
with_condition = with_condition.references(:projects) if with_condition.respond_to?(:references)
expect(with_condition.total_entries).to eq(1)
expect($query_sql.last).to match(/\bJOIN\b/)
end
it "should count with group" do
expect(Developer.group(:salary).page(1).total_entries).to eq(4)
end
it "should count with select" do
expect(Topic.select('title, content').page(1).total_entries).to eq(4)
end
it "removes :reorder for count with group" do
Project.group(:id).reorder(:id).page(1).total_entries
expect($query_sql.last).not_to match(/\ORDER\b/)
end
it "should not have zero total_pages when the result set is empty" do
expect(Developer.where("1 = 2").page(1).total_pages).to eq(1)
end
end
it "should not ignore :select parameter when it says DISTINCT" do
users = User.select('DISTINCT salary').paginate :page => 2
expect(users.total_entries).to eq(5)
end
describe "paginate_by_sql" do
it "should respond" do
expect(User).to respond_to(:paginate_by_sql)
end
it "should paginate" do
expect {
sql = "select content from topics where content like '%futurama%'"
topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 1
expect(topics.total_entries).to eq(1)
expect(topics.first.attributes.has_key?('title')).to be(false)
}.to execute(2).queries
end
it "should respect total_entries setting" do
expect {
sql = "select content from topics"
topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 1, :total_entries => 999
expect(topics.total_entries).to eq(999)
}.to execute(1).queries
end
it "defaults to page 1" do
sql = "select content from topics"
topics = Topic.paginate_by_sql sql, :page => nil, :per_page => 1
expect(topics.current_page).to eq(1)
expect(topics.size).to eq(1)
end
it "should strip the order when counting" do
expected = topics(:ar)
expect {
sql = "select id, title, content from topics order by topics.title"
topics = Topic.paginate_by_sql sql, :page => 1, :per_page => 2
expect(topics.first).to eq(expected)
}.to execute(2).queries
expect($query_sql.last).to include('COUNT')
expect($query_sql.last).not_to include('order by topics.title')
end
it "shouldn't change the original query string" do
query = 'select * from topics where 1 = 2'
original_query = query.dup
Topic.paginate_by_sql(query, :page => 1)
expect(query).to eq(original_query)
end
end
it "doesn't mangle options" do
options = { :page => 1 }
options.expects(:delete).never
options_before = options.dup
Topic.paginate(options)
expect(options).to eq(options_before)
end
it "should get first page of Topics with a single query" do
expect {
result = Topic.paginate :page => nil
result.to_a # trigger loading of records
expect(result.current_page).to eq(1)
expect(result.total_pages).to eq(1)
expect(result.size).to eq(4)
}.to execute(1).queries
end
it "should get second (inexistent) page of Topics, requiring 1 query" do
expect {
result = Topic.paginate :page => 2
expect(result.total_pages).to eq(1)
expect(result).to be_empty
}.to execute(1).queries
end
describe "associations" do
it "should paginate" do
dhh = users(:david)
expected_name_ordered = projects(:action_controller, :active_record)
expected_id_ordered = projects(:active_record, :action_controller)
expect {
# with association-specified order
result = ignore_deprecation {
dhh.projects.includes(:topics).order('projects.name').paginate(:page => 1)
}
expect(result.to_a).to eq(expected_name_ordered)
expect(result.total_entries).to eq(2)
}.to execute(2).queries
# with explicit order
result = dhh.projects.paginate(:page => 1).reorder('projects.id')
expect(result).to eq(expected_id_ordered)
expect(result.total_entries).to eq(2)
expect {
dhh.projects.order('projects.id').limit(4).to_a
}.not_to raise_error
result = dhh.projects.paginate(:page => 1, :per_page => 4).reorder('projects.id')
expect(result).to eq(expected_id_ordered)
# has_many with implicit order
topic = Topic.find(1)
expected = replies(:spam, :witty_retort)
# FIXME: wow, this is ugly
expect(topic.replies.paginate(:page => 1).map(&:id).sort).to eq(expected.map(&:id).sort)
expect(topic.replies.paginate(:page => 1).reorder('replies.id ASC')).to eq(expected.reverse)
end
it "should paginate through association extension" do
project = Project.order('id').first
expected = [replies(:brave)]
expect {
result = project.replies.only_recent.paginate(:page => 1)
expect(result).to eq(expected)
}.to execute(1).queries
end
end
it "should paginate with joins" do
result = nil
join_sql = 'LEFT JOIN developers_projects ON users.id = developers_projects.developer_id'
expect {
result = Developer.where('developers_projects.project_id = 1').joins(join_sql).paginate(:page => 1)
result.to_a # trigger loading of records
expect(result.size).to eq(2)
developer_names = result.map(&:name)
expect(developer_names).to include('David')
expect(developer_names).to include('Jamis')
}.to execute(1).queries
expect {
expected = result.to_a
result = Developer.where('developers_projects.project_id = 1').joins(join_sql).paginate(:page => 1)
expect(result).to eq(expected)
expect(result.total_entries).to eq(2)
}.to execute(1).queries
end
it "should paginate with group" do
result = nil
expect {
result = Developer.select('salary').order('salary').group('salary').
paginate(:page => 1, :per_page => 10).to_a
}.to execute(1).queries
expected = users(:david, :jamis, :dev_10, :poor_jamis).map(&:salary).sort
expect(result.map(&:salary)).to eq(expected)
end
it "should not paginate with dynamic finder" do
expect {
Developer.paginate_by_salary(100000, :page => 1, :per_page => 5)
}.to raise_error(NoMethodError)
end
describe "scopes" do
it "should paginate" do
result = Developer.poor.paginate :page => 1, :per_page => 1
expect(result.size).to eq(1)
expect(result.total_entries).to eq(2)
end
it "should paginate on habtm association" do
project = projects(:active_record)
expect {
result = ignore_deprecation { project.developers.poor.paginate :page => 1, :per_page => 1 }
expect(result.size).to eq(1)
expect(result.total_entries).to eq(1)
}.to execute(2).queries
end
it "should paginate on hmt association" do
project = projects(:active_record)
expected = [replies(:brave)]
expect {
result = project.replies.recent.paginate :page => 1, :per_page => 1
expect(result).to eq(expected)
expect(result.total_entries).to eq(1)
}.to execute(2).queries
end
it "should paginate on has_many association" do
project = projects(:active_record)
expected = [topics(:ar)]
expect {
result = project.topics.mentions_activerecord.paginate :page => 1, :per_page => 1
expect(result).to eq(expected)
expect(result.total_entries).to eq(1)
}.to execute(2).queries
end
end
it "should not paginate an array of IDs" do
expect {
Developer.paginate((1..8).to_a, :per_page => 3, :page => 2, :order => 'id')
}.to raise_error(ArgumentError)
end
it "errors out for invalid values" do |variable|
expect {
# page that results in an offset larger than BIGINT
Project.page(307445734561825862)
}.to raise_error(WillPaginate::InvalidPage, "invalid offset: 9223372036854775830")
end
end