Back to Repositories

Testing Context Query Building and Evaluation in Ransack

This test suite examines the Context class functionality within Ransack’s ActiveRecord adapter. It verifies search query building, relationship handling, and SQL generation capabilities across different ActiveRecord versions.

Test Coverage Overview

The test suite provides comprehensive coverage of Ransack’s context handling capabilities:
  • Context initialization and alias tracking
  • Relation building and evaluation
  • Correlated subquery construction for STI models
  • Complex search condition handling
  • Context sharing across multiple searches

Implementation Analysis

The testing approach focuses on verifying ActiveRecord integration points and SQL generation:
  • RSpec expectations for relation objects and SQL queries
  • Complex relationship traversal testing
  • STI model handling verification
  • Shared context behavior validation

Technical Details

Testing infrastructure includes:
  • RSpec as the testing framework
  • ActiveRecord version-specific handling
  • Custom matchers for SQL comparison
  • Table aliases and join validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Isolated context testing
  • Comprehensive edge case coverage
  • Clear test organization and grouping
  • Explicit expectation setting
  • Version compatibility handling

activerecord-hackery/ransack

spec/ransack/adapters/active_record/context_spec.rb

            
require 'spec_helper'

module Ransack
  module Adapters
    module ActiveRecord
      version = ::ActiveRecord::VERSION
      AR_version = "#{version::MAJOR}.#{version::MINOR}"

      describe Context do
        subject { Context.new(Person) }

        it 'has an Active Record alias tracker method' do
          expect(subject.alias_tracker)
          .to be_an ::ActiveRecord::Associations::AliasTracker
        end

        describe '#relation_for' do
          it 'returns relation for given object' do
            expect(subject.object).to be_an ::ActiveRecord::Relation
          end
        end

        describe '#evaluate' do
          it 'evaluates search objects' do
            s = Search.new(Person, name_eq: 'Joe Blow')
            result = subject.evaluate(s)

            expect(result).to be_an ::ActiveRecord::Relation
            expect(result.to_sql)
            .to match /#{quote_column_name("name")} = 'Joe Blow'/
          end

          it 'SELECTs DISTINCT when distinct: true' do
            s = Search.new(Person, name_eq: 'Joe Blow')
            result = subject.evaluate(s, distinct: true)

            expect(result).to be_an ::ActiveRecord::Relation
            expect(result.to_sql).to match /SELECT DISTINCT/
          end
        end

        describe '#build_correlated_subquery' do
          it 'build correlated subquery for Root STI model' do
            search = Search.new(Person, { articles_title_not_eq: 'some_title' }, context: subject)
            attribute = search.conditions.first.attributes.first
            constraints = subject.build_correlated_subquery(attribute.parent).constraints
            constraint = constraints.first

            expect(constraints.length).to eql 1
            expect(constraint.left.name).to eql 'person_id'
            expect(constraint.left.relation.name).to eql 'articles'
            expect(constraint.right.name).to eql 'id'
            expect(constraint.right.relation.name).to eql 'people'
          end

          it 'build correlated subquery for Child STI model when predicate is not_eq' do
            search = Search.new(Person, { story_articles_title_not_eq: 'some_title' }, context: subject)
            attribute = search.conditions.first.attributes.first
            constraints = subject.build_correlated_subquery(attribute.parent).constraints
            constraint = constraints.first

            expect(constraints.length).to eql 1
            expect(constraint.left.relation.name).to eql 'articles'
            expect(constraint.left.name).to eql 'person_id'
            expect(constraint.right.relation.name).to eql 'people'
            expect(constraint.right.name).to eql 'id'
          end

          it 'build correlated subquery for Child STI model when predicate is eq' do
            search = Search.new(Person, { story_articles_title_not_eq: 'some_title' }, context: subject)
            attribute = search.conditions.first.attributes.first
            constraints = subject.build_correlated_subquery(attribute.parent).constraints
            constraint = constraints.first

            expect(constraints.length).to eql 1
            expect(constraint.left.relation.name).to eql 'articles'
            expect(constraint.left.name).to eql 'person_id'
            expect(constraint.right.relation.name).to eql 'people'
            expect(constraint.right.name).to eql 'id'
          end

          it 'build correlated subquery for multiple conditions (default scope)' do
            search = Search.new(Person, { comments_body_not_eq: 'some_title' })

            # Was
            # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
            #   SELECT "comments"."disabled" FROM "comments"
            #   WHERE "comments"."disabled" = "people"."id"
            #     AND NOT ("comments"."body" != 'some_title')
            # ) ORDER BY "people"."id" DESC
            # Should Be
            # SELECT "people".* FROM "people" WHERE "people"."id" NOT IN (
            #   SELECT "comments"."person_id" FROM "comments"
            #   WHERE "comments"."person_id" = "people"."id"
            #     AND NOT ("comments"."body" != 'some_title')
            # ) ORDER BY "people"."id" DESC

            expect(search.result.to_sql).to match /.comments.\..person_id. = .people.\..id./
          end
        end

        describe 'sharing context across searches' do
          let(:shared_context) { Context.for(Person) }

          before do
            Search.new(Person, { parent_name_eq: 'A' },
              context: shared_context)
            Search.new(Person, { children_name_eq: 'B' },
              context: shared_context)
          end

          describe '#join_sources' do
            it 'returns dependent arel join nodes for all searches run against
            the context' do
              parents, children = shared_context.join_sources
              expect(children.left.name).to eq "children_people"
              expect(parents.left.name).to eq "parents_people"
            end

            it 'can be rejoined to execute a valid query' do
              parents, children = shared_context.join_sources

              expect { Person.joins(parents).joins(children).to_a }
              .to_not raise_error
            end
          end
        end

        it 'contextualizes strings to attributes' do
          attribute = subject.contextualize 'children_children_parent_name'
          expect(attribute).to be_a Arel::Attributes::Attribute
          expect(attribute.name.to_s).to eq 'name'
          expect(attribute.relation.table_alias).to eq 'parents_people'
        end

        it 'builds new associations if not yet built' do
          attribute = subject.contextualize 'children_articles_title'
          expect(attribute).to be_a Arel::Attributes::Attribute
          expect(attribute.name.to_s).to eq 'title'
          expect(attribute.relation.name).to eq 'articles'
          expect(attribute.relation.table_alias).to be_nil
        end

      end
    end
  end
end