tags:

views:

165

answers:

3

A while ago I asked "How to test obtaining a list of files within a directory using RSpec?" and although I got a couple of useful answers, I'm still stuck, hence a new question with some more detail about what I'm trying to do.

I'm writing my first RubyGem. It has a module that contains a class method that returns an array containing a list of non-hidden files within a specified directory. Like this:

files = Foo.bar :directory => './public'

The array also contains an element that represents metadata about the files. This is actually a hash of hashes generated from the contents of the files, the idea being that changing even a single file changes the hash.

I've written my pending RSpec examples, but I really have no idea how to implement them:

it "should compute a hash of the files within the specified directory"
it "shouldn't include hidden files or directories within the specified directory"
it "should compute a different hash if the content of a file changes"

I really don't want to have the tests dependent on real files acting as fixtures. How can I mock or stub the files and their contents? The gem implementation will use Find.find, but as one of the answers to my other question said, I don't need to test the library.

I really have no idea how to write these specs, so any help much appreciated!


Edit: The cache method below is the method I'm trying to test:

require 'digest/md5'
require 'find'

module Manifesto   
  def self.cache(options = {})
    directory = options.fetch(:directory, './public')
    compute_hash  = options.fetch(:compute_hash, true)
    manifest = []
    hashes = ''
    Find.find(directory) do |path|

      # Only include real files (i.e. not directories, symlinks etc.)
      # and non-hidden files in the manifest.
      if File.file?(path) && File.basename(path)[0,1] != '.'
        manifest << "#{normalize_path(directory, path)}\n"
        hashes += compute_file_contents_hash(path) if compute_hash
      end
    end

    # Hash the hashes of each file and output as a comment.
    manifest << "# Hash: #{Digest::MD5.hexdigest(hashes)}\n" if compute_hash
    manifest << "CACHE MANIFEST\n"
    manifest.reverse
  end

  # Reads the file contents to calculate the MD5 hash, so that if a file is
  # changed, the manifest is changed too.
  def self.compute_file_contents_hash(path)
    hash = ''
    digest = Digest::MD5.new
    File.open(path, 'r') do |file|
      digest.update(file.read(8192)) until file.eof
      hash += digest.hexdigest
    end
    hash
  end

  # Strips the directory from the start of path, so that each path is relative
  # to directory. Add a leading forward slash if not present.
  def self.normalize_path(directory, path)
    normalized_path = path[directory.length,path.length]
    normalized_path = '/' + normalized_path unless normalized_path[0,1] == '/'
    normalized_path
  end      
end
+1  A: 

Does MockFS help you out here? http://mockfs.rubyforge.org/

I see Fake FS was mentioned in answer to your original question, but I'm not sure that you can mock file contents with this.

Sam Phillips
+1  A: 

Could you mock the value returned by whatever method you're using to read the files? That way you could test the expected hash value, and at least ensure that the files are being read.

Edit: It looks like FakeFS does have a File.read method, so maybe that will work.

rspeicher
+1  A: 

I am going to assume that you have some method that gets all files and then computes the hash. Let's call that method get_file_hash and define it as below.

def get_file_hash
  file_hash = {}
  Find.find(Dir.pwd) do |file| 
    file_hash[file] = compute_hash(File.read(file))
  end
  file_hash
end

As I answered previously, that we are going to stub Find.find and File.read. However, we wont stub the compute_hash method since you want to check the file hash. We will let the compute_hash method create the actual hash on file contents.

describe "#get_file_hashes"

  ......

  before(:each)
    File.stubs(:find).returns(['file1', 'file2'])
    File.stubs(:read).with('file1').returns('some content')
    File.stubs(:read).with('file2').returns('other content')
  end

  it "should return the hash for all files"
@whatever_object.get_file_hashes.should eql({'file1' => "hash you are expecting for 'some content'", 'file2' => "hash you are expecting for 'other content'"})
end

end

For simplicity sake, I am just reading the file body and passing that to compute_hash method and generating a hash. However, if your compute_hash method uses some other methods on file as well for generating hash. Then you can just stub them and return a value to be passed to the compute_hash method. Although, I would be more tempted to test the compute_hash method separately if it is a public method and just stub its call in the get_file_hash method.

Regarding not showing hidden files; you will either use a library for this to leave out the private files or will have own method that does that. In former case, you dont need to write any test (assuming the library is well tested) and for latter case you need test for that seperate method not for this one.

For testing re computing the hash for files when their content changes; I guess you must have some sort of event that is triggering the re computation of hash. Just call that event method and assert the file hash match.

nas
Thanks. I just edited the question to include the actual code I'm trying to test.
John Topley
I will be forking manifesto and will send you a pull request once I have done the changes. Sounds Ok?
nas
Brilliant, that sounds perfect. Thanks!
John Topley