Back to Repositories

Testing Model Inheritance Search Functionality in Searchkick

This test suite validates inheritance behavior in Searchkick, focusing on how search functionality works across parent and child model relationships. It ensures proper indexing, searching, and autocomplete features work correctly in hierarchical model structures.

Test Coverage Overview

The test suite comprehensively covers inheritance-based search functionality in Searchkick.

Key areas tested include:
  • Child and parent model reindexing
  • Search operations across inherited models
  • Type-specific filtering
  • Autocomplete functionality
  • Missing record handling
  • Multi-index searches

Implementation Analysis

The testing approach uses a hierarchical structure with Animal as the parent class and Cat/Dog as child classes. Tests validate both isolated child model operations and aggregate parent model searches. The implementation leverages Minitest’s assertion framework with specific focus on Searchkick’s inheritance-aware features.

Technical Details

Testing tools and setup:
  • Minitest as the testing framework
  • Searchkick integration for Elasticsearch operations
  • Custom setup helpers for animal models
  • Environment-specific test indices
  • Model-specific search configurations

Best Practices Demonstrated

The test suite exemplifies several testing best practices including comprehensive edge case coverage, proper test isolation, and thorough validation of inheritance relationships. It demonstrates careful attention to index management, type-specific searches, and proper handling of missing records while maintaining clean test organization.

ankane/searchkick

test/inheritance_test.rb

            
require_relative "test_helper"

class InheritanceTest < Minitest::Test
  def setup
    super
    setup_animal
  end

  def test_child_reindex
    store_names ["Max"], Cat
    assert Dog.reindex
    assert_equal 1, Animal.search("*").size
  end

  def test_child_index_name
    assert_equal "animals_test#{ENV["TEST_ENV_NUMBER"]}", Dog.searchkick_index.name
  end

  def test_child_search
    store_names ["Bear"], Dog
    store_names ["Bear"], Cat
    assert_equal 1, Dog.search("bear").size
  end

  def test_parent_search
    store_names ["Bear"], Dog
    store_names ["Bear"], Cat
    assert_equal 2, Animal.search("bear").size
  end

  def test_force_one_type
    store_names ["Green Bear"], Dog
    store_names ["Blue Bear"], Cat
    assert_equal ["Blue Bear"], Animal.search("bear", type: [Cat]).map(&:name)
  end

  def test_force_multiple_types
    store_names ["Green Bear"], Dog
    store_names ["Blue Bear"], Cat
    store_names ["Red Bear"], Animal
    assert_equal ["Green Bear", "Blue Bear"], Animal.search("bear", type: [Dog, Cat]).map(&:name)
  end

  def test_child_autocomplete
    store_names ["Max"], Cat
    store_names ["Mark"], Dog
    assert_equal ["Max"], Cat.search("ma", fields: [:name], match: :text_start).map(&:name)
  end

  def test_parent_autocomplete
    store_names ["Max"], Cat
    store_names ["Bear"], Dog
    assert_equal ["Bear"], Animal.search("bea", fields: [:name], match: :text_start).map(&:name).sort
  end

  # def test_child_suggest
  #   store_names ["Shark"], Cat
  #   store_names ["Sharp"], Dog
  #   assert_equal ["shark"], Cat.search("shar", fields: [:name], suggest: true).suggestions
  # end

  def test_parent_suggest
    store_names ["Shark"], Cat
    store_names ["Tiger"], Dog
    assert_equal ["tiger"], Animal.search("tige", fields: [:name], suggest: true).suggestions.sort
  end

  def test_reindex
    store_names ["Bear A"], Cat
    store_names ["Bear B"], Dog
    Animal.reindex
    assert_equal 2, Animal.search("bear").size
  end

  def test_child_models_option
    store_names ["Bear A"], Cat
    store_names ["Bear B"], Dog
    Animal.reindex
    # note: the models option is less efficient than Animal.search("bear", type: [Cat, Dog])
    # since it requires two database calls instead of one to Animal
    assert_equal 2, Searchkick.search("bear", models: [Cat, Dog]).size
  end

  def test_missing_records
    store_names ["Bear A"], Cat
    store_names ["Bear B"], Dog
    Animal.reindex
    record = Animal.find_by(name: "Bear A")
    record.delete
    assert_output nil, /\[searchkick\] WARNING: Records in search index do not exist in database: Cat\/Dog \d+/ do
      result = Searchkick.search("bear", models: [Cat, Dog])
      assert_equal ["Bear B"], result.map(&:name)
      assert_equal [record.id.to_s], result.missing_records.map { |v| v[:id] }
      assert_equal [[Cat, Dog]], result.missing_records.map { |v| v[:model].sort_by(&:model_name) }
    end
    assert_empty Product.search("bear", load: false).missing_records
  ensure
    Animal.reindex
  end

  def test_inherited_and_non_inherited_models
    store_names ["Bear A"], Cat
    store_names ["Bear B"], Dog
    store_names ["Bear C"]
    Animal.reindex
    assert_equal 2, Searchkick.search("bear", models: [Cat, Product]).size

    # hits and pagination will be off with this approach (for now)
    # ideal case is add where conditions (index a, type a OR index b)
    # however, we don't know the exact index name and aliases don't work for filters
    # see https://github.com/elastic/elasticsearch/issues/23306
    # show warning for now
    # alternative is disallow inherited models with models option
    expected = Searchkick.server_below?("7.5.0") ? 3 : 2
    assert_equal expected, Searchkick.search("bear", models: [Cat, Product]).hits.size
    assert_equal expected, Searchkick.search("bear", models: [Cat, Product], per_page: 1).total_pages
  end

  # TODO move somewhere better

  def test_multiple_indices
    store_names ["Product A"]
    store_names ["Product B"], Animal
    assert_search "product", ["Product A", "Product B"], {models: [Product, Animal], conversions: false}, Searchkick
    assert_search "product", ["Product A", "Product B"], {index_name: [Product, Animal], conversions: false}, Searchkick
  end

  def test_index_name_model
    store_names ["Product A"]
    assert_equal ["Product A"], Searchkick.search("product", index_name: [Product]).map(&:name)
  end

  def test_index_name_string
    store_names ["Product A"]
    error = assert_raises Searchkick::Error do
      Searchkick.search("product", index_name: [Product.searchkick_index.name]).map(&:name)
    end
    assert_includes error.message, "Unknown model"
  end

  def test_similar
    store_names ["Dog", "Other dog"], Dog
    store_names ["Not dog"], Cat

    dog = Dog.find_by!(name: "Dog")
    assert_equal ["Other dog"], dog.similar(fields: [:name]).map(&:name)
    assert_equal ["Not dog", "Other dog"], dog.similar(fields: [:name], models: [Animal]).map(&:name).sort
    assert_equal ["Not dog"], dog.similar(fields: [:name], models: [Cat]).map(&:name).sort
  end
end