thoughtbot / shoulda-matchers

Simple one-liner tests for common Rails functionality
https://matchers.shoulda.io
MIT License
3.51k stars 912 forks source link

[Question] How to validate_absence_of enum attribute? #1327

Closed aesyondu closed 4 years ago

aesyondu commented 4 years ago

I have a similar structure code as https://github.com/thoughtbot/shoulda-matchers/issues/946, and I'm trying to find a way to validate absence of an enum value.

Model:

validates_absence_of :my_attribute, if: -> { my_condition? }

enum my_attribute: { first: 'first', second: 'second' }

I tried this but it showed an error ArgumentError: 'an arbitrary value' is not a valid my_attribute:

Rspec:

it { should validate_absence_of(:my_attribute) }

I also tried the with method but it doesn't exist on the validate_absence_of matcher (NoMethodError: undefined method with' for #<Shoulda::Matchers::ActiveModel::ValidateAbsenceOfMatcher:0x0...>):

it { should validate_absence_of(:my_attribute).with(%w(first second)) }

Or perhaps there are other alternatives other than validate_absence_of?

mcmire commented 4 years ago

Hi there! You can continue to use validate_absence_of, but you need to set up your subject to satisfy the condition.

I think it would help you to understand what validate_absence_of does under the hood. Your test:

it { should validate_absence_of(:my_attribute) }

can be expanded like so:

# this is implicitly the case in any RSpec example group, but is here for clarity
subject { described_class.new }

it "validates absence of :my_attribute" do
  subject.my_attribute = "some value"
  subject.validate
  expect(subject.errors[:my_attribute]).to include("must be blank")
end

So if you had this test, and you only wanted to run it when some condition was satisfied, how would you accomplish this? You would probably wrap it in some sort of context and override the subject to fit:

context "when some condition is satisfied" do
  subject { described_class.new(my_condition: true) }

  it "validates absence of :my_attribute" do
    subject.my_attribute = "some value"
    subject.validate
    expect(subject.errors[:my_attribute]).to include("must be blank")
  end
end

context "when some condition is not satisfied" do
  subject { described_class.new(my_condition: false) }

  it "does not validate absence of :my_attribute" do
    subject.my_attribute = "some value"
    subject.validate
    expect(subject.errors).to be_empty
  end
end

Since validate_absence_of implicitly uses subjectshould is merely a shortcut for saying expect(subject).to — you can do the same thing in this case:

context "when some condition is satisfied" do
  subject { described_class.new(my_condition: true) }

  it { should validate_absence_of(:my_attribute) }
end

context "when some condition is not satisfied" do
  subject { described_class.new(my_condition: false) }

  it { should_not validate_absence_of(:my_attribute) }
end

Obviously you'll want to change the wording of the contexts and how you change your subject to match your use case, but hopefully you get the idea. Hope this helps!

aesyondu commented 4 years ago

Thanks for the response. I forgot to mention that my snippet is already surrounded by a context, subject, and other validations.

context 'with my_condition' do
  subject { MyModel.new my_relation: build(:my_relation, :my_trait) }

  # Other validations...
  it { should validate_presence_of(:other_attribute) }
  it { should validate_absence_of(:non_enum_attribute) }

  it { should validate_absence_of(:my_attribute) } # <- error only occurs here
end

I used your suggested alternative instead, not sure why I didn't think of that, many thanks!