Back to Repositories

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

The test suite provides comprehensive coverage of will_paginate’s ActiveRecord integration, including:
  • Basic pagination functionality and parameter validation
  • Relation handling and chaining
  • Count operations and optimization
  • Association pagination (has_many, HABTM)
  • SQL-based pagination
  • Scope integration

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development patterns with ActiveRecord fixtures. Tests verify pagination behavior through detailed query counting and result validation, ensuring efficient database operations.

Key patterns include query execution tracking, fixture-based test data, and isolation of database operations for precise verification.

Technical Details

Testing tools and configuration:
  • RSpec as the testing framework
  • ActiveRecord test connector for database setup
  • Custom query counting mechanisms
  • Fixture data for users, topics, replies, and projects
  • Database transaction management

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through:
  • Isolated test cases with clear assertions
  • Comprehensive edge case coverage
  • Query count validation for performance
  • Proper test data setup and teardown
  • Modular test organization by functionality

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