Article illustration 1

In the ever-evolving landscape of Rails applications, maintaining a robust and efficient test suite remains a critical challenge for development teams. Enter Fizzy, a fresh take on Kanban board and project management tools, which not only delivers on its core functionality but also showcases an exemplary approach to testing complex Rails features. By examining Fizzy's test architecture, we uncover several sophisticated techniques that address common pain points in modern Rails development.

A Bird's Eye View of Structure

At first glance, Fizzy's test suite appears conventional, with 74 model tests, 52 controller tests, and the typical Rails directory structure. However, upon closer inspection, we notice an intriguing detail: only one system test file. This aligns with DHH's latest testing philosophy, where system tests serve as simple smoke tests rather than comprehensive end-to-end validations.

# test/system/smoke_test.rb

This minimalist approach to system testing represents a significant departure from earlier Rails conventions, where system tests often duplicated functionality already covered by unit and integration tests. Fizzy's test suite prioritizes speed and maintainability without sacrificing coverage.

Mastering Multi-Tenancy in Tests

Fizzy's URL-based multi-tenancy presents a unique testing challenge. The application elegantly solves this by establishing a selected tenant as Current.account in the test helper:

# test/test_helper.rb
setup do
  Current.account = accounts("37s")
end

This approach is complemented by setting default URL options to simulate the multi-tenant environment:

# Integration test setup
setup do
  self.default_url_options[:script_name] = "/#{ActiveRecord::FixtureSet.identify("37signals")}"
end

These configurations ensure that tests run within the proper tenant context, allowing AccountSlug::Extractor to correctly identify the tenant from URLs. This pattern offers a blueprint for testing multi-tenant applications without complex setup gymnastics.

Session Management and Magic Links

Fizzy's authentication system centers around an Identity model that connects with various User records. The test suite includes a thoughtful sign_in_as helper that handles the nuances of this authentication model:

def sign_in_as(identity)
  cookies.delete :session_token

  if identity.is_a?(User)
    user = identity
    identity = user.identity
    raise "User #{user.name} (#{user.id}) doesn't have an associated identity" unless identity
  elsif !identity.is_a?(Identity)
    identity = identities(identity)
  end

  identity.send_magic_link
  magic_link = identity.magic_links.order(id: :desc).first

  untenanted do
    post session_magic_link_url, params: { code: magic_link.code }
  end

  assert_response :redirect, "Posting the Magic Link code should grant access"

  cookie = cookies.get_cookie "session_token"
  assert_not_nil cookie, "Expected session_token cookie to be set after sign in"
end

The untenanted block is particularly clever, as it temporarily bypasses the multi-tenancy setup to allow the authentication request to proceed correctly. This pattern highlights how Fizzy's test suite adapts to the application's architectural constraints.

The magic link authentication flow is thoroughly tested with a focus on the consumption of the link itself:

# test/controllers/sessions/magic_links_controller_test.rb
test "create with sign in code" do
  identity = identities(:kevin)
  magic_link = MagicLink.create!(identity: identity)

  untenanted do
    post session_magic_link_url, params: { code: magic_link.code }

    assert_response :redirect
    assert cookies[:session_token].present?
    assert_redirected_to landing_path, "Should redirect to after authentication path"
    assert_not MagicLink.exists?(magic_link.id), "The magic link should be consumed"
  end
end

This test ensures not only successful authentication but also that the magic link is properly consumed, preventing replay attacks.

Deterministic UUID Generation

Fizzy's support for both SQLite and MySQL necessitates a sophisticated approach to UUID handling. The application uses UUIDv7 and implements a custom fixture generation system that maintains deterministic ordering:

def generate_fixture_uuid(label)
  # Generate deterministic UUIDv7 for fixtures that sorts by fixture ID
  # This allows .first/.last to work as expected in tests
  # Use the same CRC32 algorithm as Rails' default fixture ID generation
  # so that UUIDs sort in the same order as integer IDs
  fixture_int = Zlib.crc32("fixtures/#{label}") % (2**30 - 1)

  # Translate the deterministic order into times in the past, so that records
  # created during test runs are also always newer than the fixtures.
  base_time = Time.utc(2024, 1, 1, 0, 0, 0)
  timestamp = base_time + (fixture_int / 1000.0)

  uuid_v7_with_timestamp(timestamp, label)
end

This approach ensures that fixtures have predictable UUIDs that sort correctly, while test-created records are always newer than fixtures. The implementation extends to the identify method in FixturesTestHelper:

module FixturesTestHelper
  extend ActiveSupport::Concern

  class_methods do
    def identify(label, column_type = :integer)
      if label.to_s.end_with?("_uuid")
        column_type = :uuid
        label = label.to_s.delete_suffix("_uuid")
      end

      # Rails passes :string for varchar columns, so handle both :uuid and :string
      return super(label, column_type) unless column_type.in?([ :uuid, :string ])
      generate_fixture_uuid(label)
    end

    # ...
end

ActiveSupport.on_load(:active_record_fixture_set) do
  prepend(FixturesTestHelper)
end

This elegant solution allows developers to generate UUID fixtures seamlessly, maintaining the convenience of Rails' fixture system while supporting UUID primary keys.

Testing External APIs with VCR

Fizzy's test suite leverages VCR for recording HTTP interactions, with custom configurations to handle sensitive data and ensure reusable cassettes:

VCR.configure do |config|
  config.allow_http_connections_when_no_cassette = true
  config.cassette_library_dir = "test/vcr_cassettes"
  config.hook_into :webmock
  config.filter_sensitive_data("<OPEN_AI_KEY>") { Rails.application.credentials.openai_api_key || ENV["OPEN_AI_API_KEY"] }
  config.default_cassette_options = {
    match_requests_on: [ :method, :uri, :body ]
  }

  # Ignore timestamps in request bodies
  config.before_record do |i|
    if i.request&.body
      i.request.body.gsub!(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/, "<TIME>")
    end
  end

  config.register_request_matcher :body_without_times do |r1, r2|
    b1 = (r1.body || "").gsub(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/, "<TIME>")
    b2 = (r2.body || "").gsub(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/, "<TIME>")
    b1 == b2
  end

  config.default_cassette_options = {
    match_requests_on: [ :method, :uri, :body_without_times ]
  }
end

This configuration ensures that VCR cassettes remain reusable across different test runs, even when requests contain timestamps that would otherwise cause mismatches. The custom body_without_times matcher demonstrates a sophisticated understanding of VCR's capabilities.

Concurrency Testing with Activity Spike Detection

Fizzy implements tests for detecting unusual card activity patterns, showcasing an approach to concurrency testing that maintains test isolation:

test "concurrent spike creation should not create multiple spikes for a card" do
  multiple_people_comment_on(@card)
  @card.activity_spike&.destroy

  5.times.map do
    Thread.new do
      ActiveRecord::Base.connection_pool.with_connection do
        Card.find(@card.id).detect_activity_spikes
      end
    end
  end.each(&:join)

  assert_equal 1, Card::ActivitySpike.where(card: @card).count
end

The accompanying helper function demonstrates how to simulate concurrent activity while maintaining test determinism:

def multiple_people_comment_on(card, times: 4, people: users(:david, :kevin, :jz))
  perform_enqueued_jobs only: Card::ActivitySpike::DetectionJob do
    times.times do |index|
      creator = people[index % people.size]
      card.comments.create!(body: "Comment number #{index}", creator: creator)
      travel 1.second
    end
  end
end

This approach highlights Fizzy's commitment to testing real-world scenarios, including concurrent user interactions, while maintaining the reliability of the test suite.

Cross-Database Search Testing

Fizzy's test suite includes a clear_search_records helper that handles the complexities of testing full-text search across different database backends:

def clear_search_records
  if ActiveRecord::Base.connection.adapter_name == "SQLite"
    ActiveRecord::Base.connection.execute("DELETE FROM search_records")
    ActiveRecord::Base.connection.execute("DELETE FROM search_records_fts")
  else
    Search::Record::Trilogy::SHARD_COUNT.times do |shard_id|
      ActiveRecord::Base.connection.execute("DELETE FROM search_records_#{shard_id}")
    end
  end
end

This helper demonstrates a pragmatic approach to testing search functionality, accounting for both SQLite's single-table search implementation and MySQL's sharded search architecture.

Article illustration 2

Lessons for the Rails Community

Fizzy's test suite offers several valuable insights for Rails developers:

  1. Less is More: By embracing DHH's latest testing philosophy, Fizzy achieves comprehensive coverage with fewer tests, particularly in the system testing layer.

  2. Context is King: The application's approach to multi-tenancy testing demonstrates the importance of establishing proper test context, especially for applications with complex routing requirements.

  3. Pragmatic Abstraction: Custom helpers like sign_in_as and clear_search_records encapsulate complex setup logic, making tests more readable and maintainable.

  4. Database-Agnostic Testing: Fizzy's approach to UUID generation and search testing provides a blueprint for building applications that can seamlessly support multiple database backends.

  5. Concurrency Without Chaos: The activity spike detection tests show how to effectively test concurrent scenarios while maintaining test determinism.

As Rails applications grow in complexity, the patterns demonstrated in Fizzy's test offer a roadmap for maintaining test suites that are both comprehensive and efficient. The application's testing philosophy prioritizes developer experience while ensuring robust validation of critical functionality.

In an era where testing often becomes an afterthought or a source of friction, Fizzy stands as a testament to the power of thoughtful test architecture. By leveraging Rails conventions while introducing application-specific patterns, the Fizzy team has created a testing approach that is both elegant and practical.