简体   繁体   中英

Rspec Ruby on Rails Test File System in model

I have a model that has a method that looks through the filesystem starting at a particular location for files that match a particular regex. This is executed in an after_save callback. I'm not sure how to test this using Rspec and FactoryGirl. I'm not sure how to use something like FakeFS with this because the method is in the model, not the test or the controller. I specify the location to start in my FactoryGirl factory, so I could change that to a fake directory created by the test in a set up clause? I could mock the directory? I think there are probably several different ways I could do this, but which makes the most sense?

Thanks!

  def ensure_files_up_to_date
    files = find_assembly_files
    add_files = check_add_assembly_files(files)
    errors = add_assembly_files(add_files)
    if errors.size > 0 then
      return errors
    end
    update_files = check_update_assembly_files(files)
    errors = update_assembly_files(update_files)
    if errors.size > 0 then
      return errors
    else
      return []
    end
  end

 def find_assembly_files
    start_dir = self.location
    files = Hash.new
    if ! File.directory? start_dir then
      errors.add(:location, "Directory #{start_dir} does not exist on the system.")
      abort("Directory #{start_dir} does not exist on the system for #{self.inspect}")
    end
    Find.find(start_dir) do |path|
      filename = File.basename(path).split("/").last
      FILE_TYPES.each { |filepart, filehash|
        type = filehash["type"]
        vendor = filehash["vendor"]
        if filename.match(filepart) then
          files[type] = Hash.new
          files[type]["path"] = path
          files[type]["vendor"] = vendor
        end
      }
    end
    return files
  end

  def check_add_assembly_files(files=self.find_assembly_files)
    add = Hash.new
    files.each do |file_type, file_hash|
      # returns an array
      file_path = file_hash["path"]
      file_vendor = file_hash["vendor"]
      filename = File.basename(file_path)
      af = AssemblyFile.where(:name => filename)

      if af.size == 0 then
        add[file_path] = Hash.new
        add[file_path]["type"] = file_type
        add[file_path]["vendor"] = file_vendor
      end
    end
    if add.size == 0 then
      logger.error("check_add_assembly_files did not find any files to add")
      return []
    end
    return add
  end

  def check_update_assembly_files(files=self.find_assembly_files)
    update = Hash.new
    files.each do |file_type, file_hash|
      file_path = file_hash["path"]
      file_vendor = file_hash["vendor"]
      # returns an array
      filename = File.basename(file_path)
      af = AssemblyFile.find_by_name(filename)

      if !af.nil? then
        if af.location != file_path or af.file_type != file_type then
          update[af.id] = Hash.new
          update[af.id]['path'] = file_path
          update[af.id]['type'] = file_type
          update[af.id]['vendor'] = file_vendor
        end
      end
    end
    return update
  end

 def add_assembly_files(files=self.check_add_assembly_files)
    if files.size == 0 then
      logger.error("add_assembly_files didn't get any results from check_add_assembly_files")
      return []
    end
    asm_file_errors = Array.new
    files.each do |file_path, file_hash|
      file_type = file_hash["type"]
      file_vendor = file_hash["vendor"]
      logger.debug "file type is #{file_type} and path is #{file_path}"
      logger.debug FileType.find_by_type_name(file_type)
      file_type_id = FileType.find_by_type_name(file_type).id
      header = file_header(file_path, file_vendor)
      if file_vendor == "TBA" then
        check = check_tba_header(header, file_type, file_path)
        software = header[TBA_SOFTWARE_PROGRAM]
        software_version = header[TBA_SOFTWARE_VERSION]
      elsif file_vendor == "TBB" then
        check = check_tbb_header(header, file_type, file_path)
        if file_type == "TBB-ANNOTATION" then
          software = header[TBB_SOURCE]
        else
          software = "Unified"
        end
        software_version = "UNKNOWN"
      end

      if check == 0 then
        logger.error("skipping file #{file_path} because it contains incorrect values for this filetype")
        asm_file_errors.push("#{file_path} cannot be added to assembly because it contains incorrect values for this filetype")
        next
      end

      if file_vendor == "TBA" then
        xml = header.to_xml(:root => "assembly-file")
      elsif file_vendor == "TBB" then
        xml = header.to_xml
      else
        xml = ''
      end

      filename = File.basename(file_path)
      if filename.match(/~$/) then
        logger.error("Skipping a file with a tilda when adding assembly files.  filename #{filename}")
        next
      end
      assembly_file = AssemblyFile.new(
                    :assembly_id => self.id,
                    :file_type_id => file_type_id,
                    :name => filename,
                    :location => file_path,
                    :file_date => creation_time(file_path),
                    :software => software,
                    :software_version => software_version,
                    :current => 1,
                    :metadata => xml
                )

      assembly_file.save! # exclamation point forces it to raise an error if the save fails
    end # end files.each

    return asm_file_errors
  end

Quick answer: you can stub out model methods like any others. Either stub a specific instance of a model, and then stub find or whatever to return that, or stub out any_instance to if you don't want to worry about which model is involved. Something like:

it "does something" do
  foo = Foo.create! some_attributes
  foo.should_receive(:some_method).and_return(whatever)
  Foo.stub(:find).and_return(foo)
end

The real answer is that your code is too complicated to test effectively. Your models should not even know that a filesystem exists. That behavior should be encapsulated in other classes, which you can test independently. Your model's after_save can then just call a single method on that class, and testing whether or not that single method gets called will be a lot easier.

Your methods are also very difficult to test, because they are trying to do too much. All that conditional logic and external dependencies means you'll have to do a whole lot of mocking to get to the various bits you might want to test.

This is a big topic and a good answer is well beyond the scope of this answer. Start with the Wikipedia article on SOLID and read from there for some of the reasoning behind separating concerns into individual classes and using tiny, composed methods. To give you a ballpark idea, a method with more than one branch or more than 10 lines of code is too big; a class that is more than about 100 lines of code is too big.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM