How to verify controller actions are defined for all routes in a rails application?
I got curious, and the following is my attempt. It's still not accurate because it does not yet match proper format
. Also, some routes have constraints; my code doesn't yet consider.
rails console
:
todo_skipped_routes = []
valid_routes = []
invalid_routes = []
Rails.application.routes.routes.each do |route|
controller_route_name = route.defaults[:controller]
action_route_name = route.defaults[:action]
if controller_route_name.blank? || action_route_name.blank?
todo_skipped_routes << route
next
end
# TODO: maybe Rails already has a "proper" way / method to constantize this
# copied over @max answer, because I forgot to consider namespacing
controller_class = "#{controller_route_name.sub('\/', '::')}_controller".camelcase.safe_constantize
is_route_valid = !controller_class.nil? && controller_class.instance_methods(false).include?(action_route_name.to_sym)
# TODO: check also if "format" matches / gonna be "responded to" properly by the controller-action
# check also "lambda" constraints, and `request.SOMEMETHOD` constraints (i.e. `subdomain`, `remote_ip`, `host`, ...)
if is_route_valid
valid_routes << route
else
invalid_routes << route
end
end
puts valid_routes
puts invalid_routes
# puts "friendlier" version
pp invalid_routes.map(&:defaults)
# => [
# {:controller=>"sessions", :action=>"somenonexistingaction"},
# {:controller=>"posts", :action=>"criate"},
# {:controller=>"yoosers", :action=>"create"},
# ]
I am also interested to know other answers, or if there is a proper way to do this. Also, if anyone knows an improvement over my code, please let me know. Thanks :)
This builds on Jay-Ar Polidario's answer:
require 'test_helper'
class RoutesTest < ActionDispatch::IntegrationTest
Rails.application.routes.routes.each do |route|
controller, action = route.defaults.slice(:controller, :action).values
# Some routes may have the controller assigned as a dynamic segment
# We need to skip them since we can't really test them in this way
next if controller.nil?
# Skip the built in Rails 5 active_storage routes
next if 'active_storage' == controller.split('/').first
# Naive attempt to resolve the controller constant from the name
# Replacing / with :: is for namespaces
ctrl_name = "#{controller.sub('\/', '::')}_controller".camelcase
ctrl = ctrl_name.safe_constantize
# tagging SecureRandom.uuid on the end is a hack to ensure that each
# test name is unique
test "#{ctrl_name} controller exists - #{SecureRandom.uuid}" do
assert ctrl, "Controller #{ctrl_name} is not defined for #{route.name}"
end
test "#{controller} has the action #{action} - #{SecureRandom.uuid}" do
assert ctrl.respond_to?(action),
"#{ctrl_name} does not have the action '#{action}' - #{route.name}"
end if ctrl
end
end
However I would question if its actually usable for anything beyond the most trivial examples.