Back to Repositories

Testing FriendlyId Slug Candidate Generation in norman/friendly_id

This test suite validates the slug candidate functionality in FriendlyId, focusing on how the library handles URL slug generation and conflict resolution for ActiveRecord models. The tests verify various candidate configurations and edge cases for generating unique, URL-friendly identifiers.

Test Coverage Overview

The test suite provides comprehensive coverage of FriendlyId’s slug candidate system, examining multiple scenarios for generating unique URL slugs.

  • Tests slug conflict resolution with basic candidates
  • Validates handling of different candidate types (symbols, arrays, lambdas)
  • Verifies behavior with nested arrays and custom objects
  • Ensures proper handling of nil and blank candidates

Implementation Analysis

The testing approach utilizes a City model as the primary test subject, implementing various slug candidate configurations through class inheritance and method overrides.

Key patterns include:
  • Dynamic class creation for testing different candidate configurations
  • Transaction-wrapped test cases for database consistency
  • Regular expression validation for generated slugs
  • Custom object integration testing

Technical Details

Testing infrastructure includes:
  • Minitest as the testing framework
  • ActiveRecord integration for model persistence
  • Custom TestCaseClass with FriendlyId::Test module inclusion
  • Transaction-based test isolation
  • Regular expression pattern matching for slug validation

Best Practices Demonstrated

The test suite exemplifies several testing best practices in Ruby.

  • Isolated test cases with clear, specific assertions
  • Helper methods for common setup operations
  • Comprehensive edge case coverage
  • Clean test organization with descriptive names
  • Proper use of transactions for test isolation

norman/friendly_id

test/candidates_test.rb

            
require "helper"

class CandidatesTest < TestCaseClass
  include FriendlyId::Test

  class Airport
    def initialize(code)
      @code = code
    end
    attr_reader :code
    alias_method :to_s, :code
  end

  class City < ActiveRecord::Base
    extend FriendlyId
    friendly_id :slug_candidates, use: :slugged
    alias_attribute :slug_candidates, :name
  end

  def model_class
    City
  end

  def with_instances_of(klass = model_class, &block)
    transaction do
      city1 = klass.create! name: "New York", code: "JFK"
      city2 = klass.create! name: "New York", code: "EWR"
      yield city1, city2
    end
  end
  alias_method :with_instances, :with_instances_of

  test "resolves conflict with candidate" do
    with_instances do |city1, city2|
      assert_equal "new-york", city1.slug
      assert_match(/\Anew-york-([a-z0-9]+-){4}[a-z0-9]+\z/, city2.slug)
    end
  end

  test "accepts candidate as symbol" do
    klass = Class.new model_class do
      def slug_candidates
        :name
      end
    end
    with_instances_of klass do |_, city|
      assert_match(/\Anew-york-([a-z0-9]+-){4}[a-z0-9]+\z/, city.slug)
    end
  end

  test "accepts multiple candidates" do
    klass = Class.new model_class do
      def slug_candidates
        [name, code]
      end
    end
    with_instances_of klass do |_, city|
      assert_equal "ewr", city.slug
    end
  end

  test "ignores blank candidate" do
    klass = Class.new model_class do
      def slug_candidates
        [name, ""]
      end
    end
    with_instances_of klass do |_, city|
      assert_match(/\Anew-york-([a-z0-9]+-){4}[a-z0-9]+\z/, city.slug)
    end
  end

  test "ignores nil candidate" do
    klass = Class.new model_class do
      def slug_candidates
        [name, nil]
      end
    end
    with_instances_of klass do |_, city|
      assert_match(/\Anew-york-([a-z0-9]+-){4}[a-z0-9]+\z/, city.slug)
    end
  end

  test "accepts candidate with nested array" do
    klass = Class.new model_class do
      def slug_candidates
        [name, [name, code]]
      end
    end
    with_instances_of klass do |_, city|
      assert_equal "new-york-ewr", city.slug
    end
  end

  test "accepts candidate with lambda" do
    klass = Class.new City do
      def slug_candidates
        [name, [name, -> { rand 1000 }]]
      end
    end
    with_instances_of klass do |_, city|
      assert_match(/\Anew-york-\d{,3}\z/, city.friendly_id)
    end
  end

  test "accepts candidate with object" do
    klass = Class.new City do
      def slug_candidates
        [name, [name, Airport.new(code)]]
      end
    end
    with_instances_of klass do |_, city|
      assert_equal "new-york-ewr", city.friendly_id
    end
  end

  test "allows to iterate through candidates without passing block" do
    klass = Class.new model_class do
      def slug_candidates
        :name
      end
    end
    with_instances_of klass do |_, city|
      candidates = FriendlyId::Candidates.new(city, city.slug_candidates)
      assert_equal candidates.each, ["new-york"]
    end
  end

  test "iterates through candidates with passed block" do
    klass = Class.new model_class do
      def slug_candidates
        :name
      end
    end
    with_instances_of klass do |_, city|
      collected_candidates = []
      candidates = FriendlyId::Candidates.new(city, city.slug_candidates)
      candidates.each { |candidate| collected_candidates << candidate }
      assert_equal collected_candidates, ["new-york"]
    end
  end
end