Rails: Testing for Orphaned Routes

Over time, one of my large Ruby on Rails apps has accumulated a lot of cruft in it's routes file. In addition to the normal entropy that accumulates in any large app, I didn't really have a solid grasp of how routes worked when I started on this app, so it was looking pretty bad.

Well, I recently went through routes.rb and did some major housekeeping. But this app has a bunch of models, with controllers for most of them in both the default namespace and an admin namespace.

So, even after I had spent the better part of a day cleaning house, my spidey-sense was still tingling. I just didn't feel like I could be sure that everything was copacetic. And I've learned the hard way that this means it's time for some tests...

Since routes are the first interface to a web app, I figured a good place to start would be to make sure there were no orphaned routes -- routes that do not lead to a controller action. Also, I wanted a single test that would test all of my routes, so there would be no forgetting to add tests down the road.

Here's that test.


require 'test_helper'

class OrphanedRoutesTest < ActionDispatch::IntegrationTest
  ROUTES = Rails.application.routes.routes.map do |route|
    # Turn the route path spec into a string:
    # - Remove the "(.:format)" bit at the end
    # - Use "1" for all params
    path = route.path.spec.to_s.gsub(/\(\.:format\)/, "").gsub(/:[a-zA-Z_]+/, "1")
    # Route verbs are stored as regular expressions; convert them to symbols
    verb = %W{ GET POST PUT PATCH DELETE }.grep(route.verb).first.downcase.to_sym
    # Return a hash with two keys: the route path and it's verb
    { path: path, verb: verb }
  end

  test "no orphaned routes exist" do
    orphaned_routes = []

    ROUTES.each do |route|
      begin
        reset!
        # Use the route's verb to access the route's path
        request_via_redirect(route[:verb], route[:path])
      rescue ActionController::RoutingError, AbstractController::ActionNotFound
        # ActionController::RoutingError means the controller doesn't exist
        # AbstractController::ActionNotFound means the action doesn't exist
        orphaned_routes << "#{route[:verb]} #{route[:path]}"
      rescue ActiveRecord::RecordNotFound
        # This error means the controller couldn't locate the specified record.
        # This happens because we are using 1 for all the route params
        # But, it means that the route points to a valid controller action,
        # so we ignore it
      end
    end

    # Fail if we have any orphaned routes
    assert orphaned_routes.empty?, "The following routes lead to nowhere: \n\t#{orphaned_routes.uniq.join("\n\t")}"
  end
end