1

I just built a Rails 5 new app --api. I scaffolded a model and added an enum.

class Track < ApplicationRecord
  enum surface_type: [:asphalt, :gravel, :snow], _prefix: true
end

One of the scaffolded controller test looks like this:

context "with invalid params" do
  it "assigns a newly created but unsaved track as @track" do
    post :create, params: {track: invalid_attributes}, session: valid_session
    expect(assigns(:track)).to be_a_new(Track)
  end
end

I added invalid attributes at the top:

  let(:invalid_attributes) {{
    "name": "Brands Hatch",
    "surface_type": "wood"
  }}

and changed the expect line to this

expect(assigns(:track)).not_to be_valid

But the test does not work, because its not possible to create a Track object if you pass an invalid enum.

Controller action:

  def create
    @track = Track.new(track_params)

    if @track.save
      render json: @track, status: :created
    else
      render json: @track.errors, status: :unprocessable_entity
    end
  end

So how do I test this scenario?

rmcsharry
  • 5,363
  • 6
  • 65
  • 108
  • It's not possible to `create` the object, but you can instantiate it... use `.new` and then `#update_attributes` (or `#assign_attributes` with `#save`). Can you show your controller create code? – SteveTurczyn Mar 02 '17 at 23:22
  • Thanks, I added the controller code. I am guessing that whoever wrote the scaffolded test didn't think about this edge case. – rmcsharry Mar 02 '17 at 23:29
  • I'm not sure how to explain what I mean - but the scaffolded test is setup to use 'valid' and 'invalid' attributes...the implication being here that the intention is to test validations (like validates_presence_of). Enums don't use validates_presence_of, so that's why I call this an edge case. I guess I have to write some other kind of test but I'm not sure what, sigh. :( – rmcsharry Mar 02 '17 at 23:32
  • I just read this http://stackoverflow.com/questions/29198307/check-for-bad-enum-with-rspec?rq=1 and it seems without FactoryGirl this test is impossible. Once you call 'new' on the model, the enum is immediately checked to see if it exists in the enum declaration. So it seems enums work differently to model validations. – rmcsharry Mar 02 '17 at 23:49

1 Answers1

2

One way you could trap an invalid :surface_type through normal validation is by intercepting the assignment.

class Track < ApplicationRecord
  enum surface_type: [:asphalt, :gravel, :snow], _prefix: true
  attr_accessor :bad_surface_type
  validate :check_surface_type

  def surface_type=(surface)
    super surface
  rescue
    self.bad_surface_type = surface
    super nil
  end

  private

  def check_surface_type
    errors.add(:surface_type, "the value #{bad_surface_type} is not valid") if bad_surface_type
  end
end
SteveTurczyn
  • 36,057
  • 6
  • 41
  • 53
  • Clever indeed. I just found this question which shows that this was raised as an issue on Rails on Github http://stackoverflow.com/questions/37177893/rails-enum-validation-not-working-but-raise-argumenterror?noredirect=1&lq=1 and reading that issue I discovered that the RECOMMENDED use for enums is tracking INTERNAL APPLICATION STATE....not to expose them outside the app. OMG that's news to me! – rmcsharry Mar 02 '17 at 23:55
  • Huh. News to me, too. I haven't needed to worry about this because where my enums are exposed to user selection I use an `f.select` and control the options presented to the end user. So I've never felt the need to test invalid options. – SteveTurczyn Mar 02 '17 at 23:59
  • Yes, that's the reason given for raising an exception rather than a validation error. It should never be possible to set an invalid value. Except when you are building an API - so that's when the client could send invalid values. So I will probably convert the enum to a model, easier in the long term for maintainability. – rmcsharry Mar 03 '17 at 00:56