Back to Repositories

Testing ActiveRecord Pagination Scopes in Kaminari

This test suite validates pagination functionality in the Kaminari gem, focusing on ActiveRecord model extensions and scoping behaviors. It comprehensively tests page navigation, per-page limits, and pagination state management across different model types.

Test Coverage Overview

The test suite provides extensive coverage of Kaminari’s core pagination features:
  • Page method configuration and customization
  • Page number validation and boundary handling
  • Per-page limit settings and max page constraints
  • Padding and offset functionality
  • Page calculation and navigation methods

Implementation Analysis

The testing approach uses ActiveSupport::TestCase with systematic sub-test cases for each model type. It employs a structured pattern of setup/teardown hooks for data preparation and cleanup, while testing both standard and edge cases across multiple model implementations.

Notable patterns include isolated configuration testing, boundary condition validation, and comprehensive state verification.

Technical Details

Key technical components include:
  • ActiveRecord test models (User, Admin, GemDefinedModel, Device)
  • Minitest framework with ActiveSupport extensions
  • Dynamic test data generation (100 records per model)
  • Configuration manipulation for method names and limits
  • Chain-able scope testing with group operations

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated test environments with proper setup/teardown
  • Comprehensive edge case coverage
  • DRY test implementation using shared assertions
  • Clear test organization with nested contexts
  • Thorough validation of public API behavior

kaminari/kaminari

kaminari-core/test/models/active_record/scopes_test.rb

            
# frozen_string_literal: true

require 'test_helper'

if defined? ActiveRecord
  class ActiveRecordModelExtensionTest < ActiveSupport::TestCase
    test 'Changing page_method_name' do
      begin
        Kaminari.configure {|config| config.page_method_name = :per_page_kaminari }

        model = Class.new ActiveRecord::Base

        assert_respond_to model, :per_page_kaminari
        assert_not_respond_to model, :page
      ensure
        Kaminari.configure {|config| config.page_method_name = :page }
      end
    end
  end

  class ActiveRecordExtensionTest < ActiveSupport::TestCase
    def assert_first_page(relation)
      assert_equal 25, relation.count
      assert_equal 'user001', relation.first.name
    end

    def assert_blank_page(relation)
      assert_equal 0, relation.count
    end

    class << self
      def startup
        [User, GemDefinedModel, Device].each do |m|
          1.upto(100) {|i| m.create! name: "user#{'%03d' % i}", age: (i / 10)}
        end
        super
      end

      def shutdown
        [User, GemDefinedModel, Device].each(&:delete_all)
        super
      end
    end

    [User, Admin, GemDefinedModel, Device].each do |model_class|
      sub_test_case "for #{model_class}" do
        sub_test_case '#page' do
          test 'page 1' do
            assert_first_page model_class.page(1)
          end

          test 'page 2' do
            relation = model_class.page 2

            assert_equal 25, relation.count
            assert_equal 'user026', relation.first.name
          end

          test 'page without an argument' do
            assert_first_page model_class.page
          end

          test 'page < 1' do
            assert_first_page model_class.page(0)
          end

          test 'page > max page' do
            assert_blank_page model_class.page(5)
          end

          test 'invalid page as a Hash' do
            assert_first_page model_class.page(page: { hacked: 100 })
          end

          test 'invalid page as a Array' do
            assert_first_page model_class.page(page: [2, 3])
          end

          test 'ensure #order_values is preserved' do
            relation = model_class.order('id').page 1

            assert_equal ['id'], relation.order_values.uniq
          end
        end

        sub_test_case '#per' do
          test 'page 1 per 5' do
            relation = model_class.page(1).per(5)

            assert_equal 5, relation.count
            assert_equal 'user001', relation.first.name
          end

          test 'page 1 per 5 with max_per_page < 5' do
            begin
              model_class.max_paginates_per 4
              relation = model_class.page(1).per(5)

              assert_equal 4, relation.count
            ensure
              model_class.max_paginates_per nil
            end
          end

          test 'page 1 per nil (using default)' do
            assert_equal model_class.default_per_page, model_class.page(1).per(nil).count
          end

          test 'page 1 per nil with max_per_page > default_per_page' do
            begin
              model_class.max_paginates_per(30)

              assert_equal 25, model_class.page(1).per(nil).count
            ensure
              model_class.max_paginates_per(nil)
            end
          end

          test 'page 1 per nil with max_per_page < default_per_page' do
            begin
              model_class.max_paginates_per(10)

              assert_equal 10, model_class.page(1).per(nil).count
            ensure
              model_class.max_paginates_per(nil)
            end
          end

          test 'page 1 per 0' do
            assert_equal 0, model_class.page(1).per(0).count
          end

          # I know it's a bit strange to have this here, but I couldn't find any better place for this case
          sub_test_case 'when max_per_page is given via model class, and `per` is not actually called' do
            test 'with max_per_page > default_per_page' do
              begin
                model_class.max_paginates_per(200)

                assert_equal 25, model_class.page(1).count
              ensure
                model_class.max_paginates_per(nil)
              end
            end

            test 'with max_per_page < default_per_page' do
              begin
                model_class.max_paginates_per(5)

                assert_equal 5, model_class.page(1).count
              ensure
                model_class.max_paginates_per(nil)
              end
            end
          end
        end

        sub_test_case '#max_paginates_per' do
          setup do
            model_class.max_paginates_per(10)
          end
          teardown do
            model_class.max_paginates_per(nil)
          end

          sub_test_case 'calling max_paginates_per() after per()' do
            test 'when #max_paginates_per is greater than #per' do
              assert_equal 15, model_class.page(1).per(15).max_paginates_per(20).count
            end

            test 'when #per is greater than #max_paginates_per' do
              assert_equal 20, model_class.page(1).per(30).max_paginates_per(20).count
            end

            test 'when nil is given to #per and #max_paginates_per is specified' do
              assert_equal 20, model_class.page(1).per(nil).max_paginates_per(20).count
            end
          end

          sub_test_case 'calling max_paginates_per() before per()' do
            test 'when #max_paginates_per is greater than #per' do
              assert_equal 15, model_class.page(1).max_paginates_per(20).per(15).count
            end

            test 'when #per is greater than #max_paginates_per' do
              assert_equal 20, model_class.page(1).max_paginates_per(20).per(30).count
            end

            test 'when nil is given to #per and #max_paginates_per is specified' do
              assert_equal 20, model_class.page(1).max_paginates_per(20).per(nil).count
            end
          end

          sub_test_case 'calling max_paginates_per() without per()' do
            test 'when #max_paginates_per is greater than the default per_page' do
              assert_equal 20, model_class.page(1).max_paginates_per(20).count
            end

            test 'when #max_paginates_per is less than the default per_page' do
              assert_equal 25, model_class.page(1).max_paginates_per(30).count
            end
          end
        end

        sub_test_case '#padding' do
          test 'page 1 per 5 padding 1' do
            relation = model_class.page(1).per(5).padding(1)

            assert_equal 5, relation.count
            assert_equal 'user002', relation.first.name
          end

          test 'page 1 per 5 padding "1" (as string)' do
            relation = model_class.page(1).per(5).padding('1')

            assert_equal 5, relation.count
            assert_equal 'user002', relation.first.name
          end

          test 'page 19 per 5 padding 5' do
            relation = model_class.page(19).per(5).padding(5)

            assert_equal 19, relation.current_page
            assert_equal 19, relation.total_pages
          end

          test 'Negative padding' do
            assert_raise(ArgumentError) { model_class.page(1).per(5).padding(-1) }
          end
        end

        sub_test_case '#total_pages' do
          test 'per 25 (default)' do
            assert_equal 4, model_class.page.total_pages
          end

          test 'per 7' do
            assert_equal 15, model_class.page(2).per(7).total_pages
          end

          test 'per 65536' do
            assert_equal 1, model_class.page(50).per(65536).total_pages
          end

          test 'per 0' do
            assert_raise Kaminari::ZeroPerPageOperation do
              model_class.page(50).per(0).total_pages
            end
          end

          test 'per -1 (using default)' do
            assert_equal 4, model_class.page(5).per(-1).total_pages
          end

          test 'per "String value that can not be converted into Number" (using default)' do
            assert_equal 4, model_class.page(5).per('aho').total_pages
          end

          test 'with max_pages < total pages count from database' do
            begin
              model_class.max_pages 3

              assert_equal 3, model_class.page.total_pages
            ensure
              model_class.max_pages nil
            end
          end

          test 'with max_pages > total pages count from database' do
            begin
              model_class.max_pages 11

              assert_equal 4, model_class.page.total_pages
            ensure
              model_class.max_pages nil
            end
          end

          test 'with max_pages is nil (default)' do
            model_class.max_pages nil

            assert_equal 4, model_class.page.total_pages
          end

          test 'with per(nil) using default' do
            assert_equal 4, model_class.page.per(nil).total_pages
          end
        end

        sub_test_case '#current_page' do
          test 'any page, per 0' do
            assert_raise Kaminari::ZeroPerPageOperation do
              model_class.page.per(0).current_page
            end
          end

          test 'page 1' do
            assert_equal 1, model_class.page.current_page
          end

          test 'page 2' do
            assert_equal 2, model_class.page(2).per(3).current_page
          end
        end

        sub_test_case '#current_per_page' do
          test 'per 0' do
            assert_equal 0, model_class.page.per(0).current_per_page
          end

          test 'no per specified' do
            assert_equal model_class.default_per_page, model_class.page.current_per_page
          end

          test 'per specified as 42' do
            assert_equal 42, model_class.page.per(42).current_per_page
          end
        end

        sub_test_case '#next_page' do
          test 'page 1' do
            assert_equal 2, model_class.page.next_page
          end

          test 'page 5' do
            assert_nil model_class.page(5).next_page
          end
        end

        sub_test_case '#prev_page' do
          test 'page 1' do
            assert_nil model_class.page.prev_page
          end

          test 'page 3' do
            assert_equal 2, model_class.page(3).prev_page
          end

          test 'page 5' do
            assert_nil model_class.page(5).prev_page
          end
        end

        sub_test_case '#first_page?' do
          test 'on first page' do
            assert_true model_class.page(1).per(10).first_page?
          end

          test 'not on first page' do
            assert_false model_class.page(5).per(10).first_page?
          end
        end

        sub_test_case '#last_page?' do
          test 'on last page' do
            assert_true model_class.page(10).per(10).last_page?
          end

          test 'within range' do
            assert_false model_class.page(1).per(10).last_page?
          end

          test 'out of range' do
            assert_false model_class.page(11).per(10).last_page?
          end
        end

        sub_test_case '#out_of_range?' do
          test 'on last page' do
            assert_false model_class.page(10).per(10).out_of_range?
          end

          test 'within range' do
            assert_false model_class.page(1).per(10).out_of_range?
          end

          test 'out of range' do
            assert_true model_class.page(11).per(10).out_of_range?
          end
        end

        sub_test_case '#count' do
          test 'page 1' do
            assert_equal 25, model_class.page.count
          end

          test 'page 2' do
            assert_equal 25, model_class.page(2).count
          end
        end

        test 'chained with .group' do
          relation = model_class.group('age').page(2).per 5
          # 0..10
          assert_equal 11, relation.total_count
          assert_equal 3, relation.total_pages
        end

        test 'activerecord descendants' do
          assert_not_equal 0, ActiveRecord::Base.descendants.length
        end
      end
    end
  end
end