Rails嵌套路由下EntitiesController RSpec测试报错排查
问题根因
所有报错核心来自两点:
- 你配置的是
schemas嵌套entities的路由,所有EntitiesController的动作都依赖路由传入的params[:schema_id]定位所属Schema,但你写测试发请求时完全没有传这个外层路由参数,甚至把schema_id错塞到了entity的属性参数里,直接导致路由匹配失败、控制器拿不到Schema实例。 - 代码里硬编码查找id=1的Schema、控制器查找Schema后没做空判断,进一步放大了问题。
逐个对应报错:
index动作的NoMethodError:发get :index时没传任何参数,既没有schema_id也没有id,Schema.find_by返回nil,调用.entities直接触发空指针错误。new动作的ActiveRecord::RecordNotFound:发get :new时没传schema_id,Schema.find(params[:schema_id])拿不到主键值,直接抛出找不到记录的异常。- 两个
create动作的UrlGenerationError:嵌套路由的实体创建路径格式是/schemas/:schema_id/entities,必须在外层参数传schema_id才能匹配路由,你把schema_id放在entity属性哈希里,路由层直接匹配失败,根本进不到控制器逻辑。 - 额外问题:你路由里单独定义的
get 'entities/index', to:'entities#index'是重复冗余路由,这个路由没有schema_id参数,就算手动访问也会触发空指针错误,建议删除。
修复方案
1. 优化路由配置
删除冗余的单独entities index路由,保留嵌套路由即可:
Rails.application.routes.draw do get 'joins/index' get 'fields/index' # 删除下面这行冗余路由 # get 'entities/index', to:'entities#index' root "schemas#index" resources :schemas do resources :entities do resources :fields do resources :joins end end end end
2. 优化控制器代码
抽取公共的Schema查找逻辑,增加空判断避免空指针,去掉重复代码:
class EntitiesController < ApplicationController # 所有动作都依赖所属Schema,统一前置加载 before_action :find_schema, only: [:index, :new, :create] def index @entities = if params[:search] @schema.entities.search(params[:search]).order("created_at DESC") else @schema.entities.order("created_at DESC") end end def new @entity = Entity.new end def create @entity = @schema.entities.new(entity_params) if @entity.save redirect_to schema_entities_path(@schema) else # 保存失败直接渲染表单,不要重定向丢了校验错误信息 render :new, status: :unprocessable_entity end end private def find_schema @schema = Schema.find_by(id: params[:schema_id]) # 找不到Schema直接重定向首页,避免nil错误 redirect_to root_path, alert: "所属数据集不存在" unless @schema end def entity_params params.require(:entity).permit(:clientId, :name, :description, :mnemonic) end end
3. 修正测试代码
测试发请求时必须在外层参数传入schema_id,不要硬编码记录id,直接用before块创建的实例取id:
# frozen_string_literal: true require 'rails_helper' RSpec.describe EntitiesController, type: :controller do before(:each) do # 直接取create返回的实例,不要硬编码查找id=1,避免id不匹配问题 @schema = Schema.create(name:'testEntity', schemaId:'', schemaMnemonic:'') @entity = Entity.create(clientId:'1', name:'test', description:'test', mnemonic:'test', schema: @schema) end describe 'GET #index' do it '返回200状态码' do # 嵌套路由必须传外层schema_id参数 get :index, params: { schema_id: @schema.id } expect(response).to have_http_status(200) expect(assigns(:entities)).to match_array([@entity]) end end describe 'GET #new' do it '返回200状态码' do get :new, params: { schema_id: @schema.id } expect(response).to have_http_status(200) end end describe 'POST #create' do it '参数合法时重定向到实体列表页' do valid_params = { schema_id: @schema.id, entity: { clientId:'1', name:'test_create', description:'test', mnemonic:'test' } } post :create, params: valid_params expect(response).to have_http_status(302) expect(response).to redirect_to(schema_entities_path(@schema)) end it '参数非法时返回表单页状态码' do invalid_params = { schema_id: @schema.id, entity: { name: '' } } post :create, params: invalid_params expect(response).to have_http_status(:unprocessable_entity) end end end
注意事项
- 嵌套路由的所有请求,必须把父资源id作为外层请求参数传递,不能放在子资源的属性哈希里,否则会出现路由匹配失败、父资源找不到的问题。
- 测试代码不要硬编码数据库记录id,测试执行时数据库会定期清理,id不一定从1开始,直接使用创建返回的实例对象取id是最稳妥的方式。
- 控制器加载关联资源后一定要做空判断,避免找不到资源时触发nil的NoMethodError。
内容的提问来源于stack exchange,提问作者Animesh Arya
相关产品推荐
相关产品推荐

