克隆新创建的记录时忽略验证

Mis*_*cha 5 unit-testing ruby-on-rails ruby-on-rails-3

我有一个模型UserFilebelongs_to一个Folder:

class UserFile < ActiveRecord::Base
  has_attached_file :attachment
  belongs_to :folder

  validates_attachment_presence :attachment
  validates_presence_of :folder_id

  def copy(target_folder)
    new_file = self.clone
    new_file.folder = target_folder
    new_file.save!
  end
end
Run Code Online (Sandbox Code Playgroud)

以下测试意外失败:

test 'cannot copy a file to anything other than a folder' do
  folder = Factory(:folder)
  file1 = UserFile.create(:attachment => File.open("#{Rails.root}/test/fixtures/textfile.txt"), :folder => Folder.root)
  file2 = UserFile.find(file1)

  # Should pass, but fails
  assert_raise(ActiveRecord::RecordInvalid) { file1.copy(nil) }

  # Same record, but this DOES pass
  assert_raise(ActiveRecord::RecordInvalid) { file2.copy(nil) }

  assert file1.copy(folder)
end
Run Code Online (Sandbox Code Playgroud)

validates_presence_of :folder_id使用新创建对象时被忽略,但是当我做了ActiveRecord#find它的工作.我认为这是与调用clonecopy方法,但我不能弄明白.有谁知道发生了什么或如何使测试通过?

Vik*_*rón 3

Mischa, cloning is a beast.

record.errors is memoized and the @errors instance variable gets cloned too.

file1.errors = new_file.errors
Run Code Online (Sandbox Code Playgroud)

this will be non-nil since create called validations on file1.

now what happens when you clone file1 and say new_file.save!? Deep inside valid? calls errors.clear on new_file but it still points to the same error object as file1. Now viciously, the presence validator is implemented like this:

def validate(record)
   record.errors.add_on_blank(attributes, options)
end
Run Code Online (Sandbox Code Playgroud)

which (obviously) can only access errors.base http://apidock.com/rails/ActiveModel/Errors/add_on_blank

so, although, the validations do run on new_file as the record, the presence validation passes since

new_file.errors.instance_eval { @base } == file1
Run Code Online (Sandbox Code Playgroud)

and for file1.folder_id is NOT blank.

Now, your second test passes because if you read the file entry from the db, file2.errors is nil so when you clone it and call validations on the clone, the errors object is created anew with the correct base (the clone) for which folder_id will be blank because of the line new_file.folder = target_folder.

your problem is solved by simply adding

def copy(target_folder)
    new_file = self.clone
    new_file.instance_eval { @errors = nil } # forces new error object on clone
    new_file.folder = target_folder
    new_file.save!
end
Run Code Online (Sandbox Code Playgroud)

hope this helped