Back to Repositories

Testing Sequential Slug Generation and History Tracking in FriendlyId

This test suite validates the sequential slug generation functionality in FriendlyId, focusing on how the library handles duplicate slugs by appending sequential numbers. It covers core slug generation, history tracking, and scoped slugging scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of FriendlyId’s sequential slugging capabilities.

Key areas tested include:
  • Basic sequential slug generation for duplicate entries
  • Handling missing sequence numbers
  • Custom column name support
  • Numeric slug handling (beginning/ending with numbers)
  • Custom sequence separator implementation
  • Integration with history and scoping features

Implementation Analysis

The testing approach employs a modular structure using Ruby’s Minitest framework. Tests are organized into three main classes: SequentiallySluggedTest, SequentiallySluggedTestWithHistory, and SequentiallySluggedTestWithScopedHistory.

Each class utilizes transaction blocks for database isolation and implements shared behavior through module inclusion.

Technical Details

Testing infrastructure includes:
  • ActiveRecord as the ORM layer
  • Minitest as the testing framework
  • FriendlyId::Test module for shared functionality
  • Transaction-based test isolation
  • Multiple model classes (Article, Restaurant, City) for different scenarios

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Isolated test cases with clear, specific assertions
  • Comprehensive edge case coverage
  • Proper test organization and inheritance
  • Clean setup and teardown through transactions
  • Descriptive test names that clearly indicate functionality being tested

norman/friendly_id

test/sequentially_slugged_test.rb

            
require "helper"

class Article < ActiveRecord::Base
  extend FriendlyId
  friendly_id :name, use: :sequentially_slugged
end

class SequentiallySluggedTest < TestCaseClass
  include FriendlyId::Test
  include FriendlyId::Test::Shared::Core

  def model_class
    Article
  end

  test "should generate numerically sequential slugs" do
    transaction do
      records = 12.times.map { model_class.create! name: "Some news" }
      assert_equal "some-news", records[0].slug
      (1...12).each { |i| assert_equal "some-news-#{i + 1}", records[i].slug }
    end
  end

  test "should cope when slugs are missing from the sequence" do
    transaction do
      record_1 = model_class.create!(name: "A thing")
      record_2 = model_class.create!(name: "A thing")
      record_3 = model_class.create!(name: "A thing")

      assert_equal "a-thing", record_1.slug
      assert_equal "a-thing-2", record_2.slug
      assert_equal "a-thing-3", record_3.slug

      record_2.destroy

      record_4 = model_class.create!(name: "A thing")

      assert_equal "a-thing-4", record_4.slug
    end
  end

  test "should cope with strange column names" do
    model_class = Class.new(ActiveRecord::Base) do
      self.table_name = "journalists"
      extend FriendlyId
      friendly_id :name, use: :sequentially_slugged, slug_column: "strange name"
    end

    transaction do
      record_1 = model_class.create! name: "Lois Lane"
      record_2 = model_class.create! name: "Lois Lane"

      assert_equal "lois-lane", record_1.attributes["strange name"]
      assert_equal "lois-lane-2", record_2.attributes["strange name"]
    end
  end

  test "should correctly sequence slugs that end in a number" do
    transaction do
      record1 = model_class.create! name: "Peugeuot 206"
      assert_equal "peugeuot-206", record1.slug
      record2 = model_class.create! name: "Peugeuot 206"
      assert_equal "peugeuot-206-2", record2.slug
    end
  end

  test "should correctly sequence slugs that begin with a number" do
    transaction do
      record1 = model_class.create! name: "2010 to 2015 Records"
      assert_equal "2010-to-2015-records", record1.slug
      record2 = model_class.create! name: "2010 to 2015 Records"
      assert_equal "2010-to-2015-records-2", record2.slug
    end
  end

  test "should sequence with a custom sequence separator" do
    model_class = Class.new(ActiveRecord::Base) do
      self.table_name = "novelists"
      extend FriendlyId
      friendly_id :name, use: :sequentially_slugged, sequence_separator: ":"
    end

    transaction do
      record_1 = model_class.create! name: "Julian Barnes"
      record_2 = model_class.create! name: "Julian Barnes"

      assert_equal "julian-barnes", record_1.slug
      assert_equal "julian-barnes:2", record_2.slug
    end
  end

  test "should not generate a slug when canidates set is empty" do
    model_class = Class.new(ActiveRecord::Base) do
      self.table_name = "cities"
      extend FriendlyId
      friendly_id :slug_candidates, use: [:sequentially_slugged]

      def slug_candidates
        [name, [name, code]]
      end
    end
    transaction do
      record = model_class.create!(name: nil, code: nil)
      assert_nil record.slug
    end
  end

  test "should not generate a slug when the sluggable attribute is blank" do
    record = model_class.create!(name: "")
    assert_nil record.slug
  end
end

class SequentiallySluggedTestWithHistory < TestCaseClass
  include FriendlyId::Test
  include FriendlyId::Test::Shared::Core

  class Article < ActiveRecord::Base
    extend FriendlyId
    friendly_id :name, use: [:sequentially_slugged, :history]
  end

  Journalist = Class.new(ActiveRecord::Base) do
    extend FriendlyId
    friendly_id :name, use: [:sequentially_slugged, :history], slug_column: "strange name"
  end

  def model_class
    Article
  end

  test "should work with regeneration with history when slug already exists" do
    transaction do
      record1 = model_class.create! name: "Test name"
      record2 = model_class.create! name: "Another test name"
      assert_equal "test-name", record1.slug
      assert_equal "another-test-name", record2.slug

      record2.name = "Test name"
      record2.slug = nil
      record2.save!
      assert_equal "test-name-2", record2.slug
    end
  end

  test "should work with regeneration with history when 2 slugs already exists and the second is changed" do
    transaction do
      record1 = model_class.create! name: "Test name"
      record2 = model_class.create! name: "Test name"
      record3 = model_class.create! name: "Another test name"
      assert_equal "test-name", record1.slug
      assert_equal "test-name-2", record2.slug
      assert_equal "another-test-name", record3.slug

      record2.name = "One more test name"
      record2.slug = nil
      record2.save!
      assert_equal "one-more-test-name", record2.slug

      record3.name = "Test name"
      record3.slug = nil
      record3.save!
      assert_equal "test-name-3", record3.slug
    end
  end

  test "should cope with strange column names" do
    transaction do
      record_1 = Journalist.create! name: "Lois Lane"
      record_2 = Journalist.create! name: "Lois Lane"

      assert_equal "lois-lane", record_1.attributes["strange name"]
      assert_equal "lois-lane-2", record_2.attributes["strange name"]
    end
  end
end

class City < ActiveRecord::Base
  has_many :restaurants
end

class Restaurant < ActiveRecord::Base
  extend FriendlyId
  belongs_to :city
  friendly_id :name, use: [:sequentially_slugged, :scoped, :history], scope: :city
end

class SequentiallySluggedTestWithScopedHistory < TestCaseClass
  include FriendlyId::Test
  include FriendlyId::Test::Shared::Core

  def model_class
    Restaurant
  end

  test "should work with regeneration with scoped history" do
    transaction do
      city1 = City.create!
      City.create!
      record1 = model_class.create! name: "Test name", city: city1
      record2 = model_class.create! name: "Test name", city: city1

      assert_equal "test-name", record1.slug
      assert_equal "test-name-2", record2.slug

      record2.name = "Another test name"
      record2.slug = nil
      record2.save!

      record3 = model_class.create! name: "Test name", city: city1
      assert_equal "test-name-3", record3.slug
    end
  end
end