Rspecs テストで HTTP 動詞を使用すると、float の配列が間違って解析される
概要
次のクラスを定義しました。
ショップ.rb:
class Shop
field: :reputation, Float
embeds_one :location, class_name: "Location"
accepts_nested_attributes_for :location
end
場所.rb:
class Location
include Mongoid::Document
field :address, type: String
field :coordinates, type: Array
field :place_id, type: String
validate :coordinates_must_be_pair_of_float
private
def coordinates_must_be_pair_of_float
unless coordinates.is_a?(Array) && coordinates.size == 2
errors.add(:coordinates, "must be an array with exactly two elements")
return
end
coordinates.each do |coord|
unless coord.is_a?(Float)
errors.add(:coordinates, "must contain only integers")
return
end
end
end
end
shop_controller.rb 内:
def create
shop = Shop.new(shop_params)
if shop.save
render json: { message: 'Shop created successfully', shop: shop }, status: :created
else
render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
end
end
private
def shop_params
params.require(:shop).permit(
:reputation,
location_attributes: [:address, :place_id, coordinates: []],
)
end
最後に、shop_spect.rb で次のようにします。
let(:location) { { address: "St. My Street", coordinates: [-100.0, 100.0], place_id: "12345" } }
describe "POST /shop" do
it "creates a new shop" do
shop_data = {
reputation: 800
location_attributes: location,
}
post "/shop", params: { shop: shop_data }
if response.status == 422
errors = JSON.parse(response.body)["errors"]
puts "Validation errors: #{errors.join(', ')}" # Display the error messages
end
expect(response).to have_http_status(201)
次のようにcurlを使用してPOSTを作成すると、次のようになります。
curl -X POST \
-H "Content-Type: application/json" \
-d '{
"shop": {
"reputation": 800,
"location_attributes": {
"address": "My Street",
"coordinates": [-100.0, 100.0],
"place_id": "12345"
},
}
}' \
"http://localhost:3000/shop"
すべて正常に動作しますが、テストはエラー コード 422 で失敗しました。つまり、インスタンスを保存できませんでした。しばらくして、私は問題に気づきました。座標配列は、レピュテーションが処理されるのと同じ方法で処理されていません。座標配列に含まれる値のタイプは、エンコーディング、UTF8 でした。 。
また、これはテストの params の値です。
{:shop=>{:price=>800, :location_attributes=>{:address=>"My Street", :coordinates=>[-100.0, 100.0], :place_id=>"12345"}}}
これはコントローラーの params の値です。
{"shop"=>{"reputation"=>"800", "location_attributes"=>{"address"=>"My Street", "coordinates"=>["-100.0", "100.0"], "place_id"=>"12345"} }, "price"=>"800"}, "controller"=>"advert", "action"=>"create"}
最後に、これは、curl を使用してリクエストを作成したときのコントローラーの params の値です。
{"shop"=>{"reputation"=>800, "location_attributes"=>{"address"=>"My Street", "coordinates"=>[-100.0, 100.0], "place_id"=>"12345"}}, "controller"=>"advert", "action"=>"create"}
明らかにタグは文字列に変換されますが、rspec で post を使用するときに整数と浮動小数点も文字列に変換されるのはなぜですか?
したがって、場所クラスの検証は成功しませんでした。この問題を解決するには、コントローラーを次のように変更する必要がありました。 ショップコントローラー.rb:
def create
shop = Shop.new(shop_params)
shop.location.coordinates.map!(&:to_f)
if shop.save
render json: { message: 'Shop created successfully', shop: shop }, status: :created
else
render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
end
end
private
def shop_params
params.require(:shop).permit(
:reputation,
location_attributes: [:address, :place_id, coordinates: []],
)
end
なぜこんなことが起こるのか分かりません。レピュテーション フィールドの場合と同様に、パーサーが配列の内容を Float 値としてではなく、エンコードされた UTF8 データとして解釈するのはなぜですか?
また、shop_paramsを定義する方法はありますか?次の定義が無効である理由:
def shop_params
params.require(:shop).permit(
:reputation,
location_attributes: [:address, :place_id, :coordinates],
)
end
解決策
これは RSpec とはほとんど関係がありません。
post メソッドのデフォルトは application/x-www-form-urlencoded (Rails では :html 形式として扱われる) であるため、仕様では実際には JSON リクエストを送信していません。
JSON を送信するには、以下を使用します。
post "/shop", params: { shop: shop_data }, format: :json
これは実際には、RSpec がラップするだけの基礎となる ActionDispatch::IntegrationTest によって提供されるヘルパーです。
文字列を取得しているのは、HTTP フォーム データ パラメーターが実際には入力されていないためです。これらは文字列形式のキーと値の単なるペアです。
さらに、コントローラーは実際にはリクエスト形式を JSON に制限していないため、このバグがすり抜けてしまいます。代わりに MimeResponds を使用して、ActionController::UnknownFormat 例外が確実に発生するようにします。
class ShopsController < ApplicationController
# ...
def create
shop = Shop.new(shop_params)
# don't do this - it's just silly to make the controller fix
# bad modeling
# shop.location.coordinates.map!(&:to_f)
# this will raise if the client requests HTML
respond_to :json do
if shop.save
render json: { message: 'Shop created successfully', shop: shop }, status: :created
else
render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
end
end
end
end
配列型を使用するのは明らかに悪い考えです。あまり不安定ではなく、型キャストと 2 つの個別の属性を提供するため、2 つの float 型フィールドを定義するだけで、配列から取得せずに実際にコード内で lat または lng を取得できるようになります。
class Location
include Mongoid::Document
field :address, type: String
field :place_id, type: String
field :latitude, type: Float
field :longitude, type: Float
validates :longitude, :latitude, presence: true,
numericality: true
# Sets the latitude and longitude from an array or list
def coordinates=(*args)
self.latitude, self.longitude = *args.flatten
end
def coordinates
[self.latitude, self.longitude]
end
end
本当にパラメータを配列として受け入れたい場合は、それをホワイトリストに登録する必要があります。また、配列は許可されたスカラー型ではありません。
def shop_params
params.require(:shop).permit(
:reputation,
location_attributes: [
:address,
:place_id,
coordinates: []
]
)
end