Techioz Blog

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