views:

279

answers:

5

I've been struggling to adapt my standard approach for test-driving .NET code to Ruby.

As an example, I am writing a class that will:

grab all *.markdown files from a directory
  foreach file:
    extract code samples from file
    save code to file.cs in output directory

Normally for .NET I'd end up with something like:

class ExamplesToCode {
  public ExamplesToCode(IFileFinder finder, IExampleToCodeConverter converter) { ... }
  public void Convert(string exampleDir, string targetDir) { ... }
}

In my test (written first), I'd mock finder and converter. Then I'd stub out finder.FindFiles("*.markdown") to return say ["file1", "file2"], and check converter.Convert("file1", targetDir) and converter.Convert("file2", targetDir) was called.

Where I struggle applying this to Ruby is that Ruby tends to use blocks and internal iterators (e.g. array.each { |x| puts x }), and including modules over constructor injection. I'm not sure on how to unit test code in those cases (without setting up a full integration test), and the .NET approach just seems incredibly un-rubyish; it seems to fight the way Ruby naturally works.

Any suggestions on how to do this the Ruby way? An example of a Ruby test for this example would be great.

A: 

Even in ruby there are only two ways to decouple this code: DI or a service locator. Of the two I still prefer DI as you have described.

I'm not sure of the ruby idioms but I suspect that they would not bother with the IFileFinder abstraction, instead directly calling Dir["*.makrkdown"] and then rewriting that in the test.

liammclennan
By "rewriting that in the test", do you mean monkey-patching Dir??
David Tchepak
+2  A: 

Before answering the question on providing a way on how to do this in Ruby I'd like to clear up some misunderstandings.

Firstly, I would not say that there is a "Ruby way" of testings things like this any more than there is a strict way of testing something like this in .NET (which, admittedly I have not used for years). One could take an interaction-based (mocking) approach or, as you said, take more of a state-based approach by creating an integration test that exercises all three classes at once. The tradeoffs between the two approaches I think are language agnostic. Ruby has many mocking frameworks that would allow you to take an interaction-based approach if that is what you are most comfortable with. (I typically use the one that ships with RSpec.)

Secondly, I don't think "including modules over constructor injection" is an accurate statement. Modules are an extra tool available to you in Ruby but they by no means replace good OO design with object composition. I pass in dependencies to my initializers all the time in Ruby since it makes them easier to test and more reusable. I will generally default the dependency in the argument list however like so def initialize(converter=CodeConverter.new).

Now, to answer your question. What liammclennan said about using Dir::[] is correct- the finder is not needed. So the question is, how do you write tests for methods that calls Dir::[]? You have three options: 1) Use one of the above mentioned mocking libraries and stub Dir::[] (this is a simple and easy approach), 2) Write the files to disk and verify they are read (ick), or 3) Use a library like FakeFS to prevent disk IO but still allows you to write a natural looking test. The following example is one possible way of writing/testing this (using RSpec and FakeFS) that is somewhat parallel to your original design:

class CodeExtractor

  def self.extract_dir(example_dir, target_dir)
    Dir[example_dir + "/*.md"].each do |filename|
      self.extract(filename, target_dir)
    end
  end

  def self.extract(*args)
    self.new(*args).extract
  end

  def extract(filename, target_dir)
    # ...
  end
end

# The spec...
require 'fakefs/spec_helpers'
describe CodeExtractor do
  include FakeFS::SpecHelpers

  describe '::extract_dir' do
    it "extracts each markdown file in the provided example dir" do
      FileUtils.touch(["foo.md", "bar.md"])
      CodeExtractor.should_receive(:extract).with(Dir.pwd + "/foo.md","/target")
      CodeExtractor.should_receive(:extract).with(Dir.pwd + "/bar.md","/target")
      CodeExtractor.extract_dir(Dir.pwd, "/target")
    end
  end

  describe '#extract' do
    it "blah blah blah" do
      # ...
    end
  end
end

Of course, there is the question of if such a test adds enough value to merit it's existence. I don't think I'll go into that though.... If you do decide to use FakeFS be aware that the stacktraces from errors can be non-helpful since the FS is faked out when RSpec tries to get the line number off the non-existent FS. :) Coincidently, I have some code that reads and parses markdown slides on github. The specs may serve as further examples of how you can approach testing things like this in Ruby. HTH, and good luck.

Ben Mabey
The original poster's comment about "including modules over dependency injection" is based on this article: http://fabiokung.com/2010/05/06/ruby-and-dependency-injection-in-a-dynamic-world/ So although you can still do DI through constructor, he saw merit in the module option presented in the article.
mkmurray
Thanks Ben. Very helpful. While I wrote "the Ruby way", I really meant "a Ruby way". I realise there's lots of ways to do it, but some ways suit different platforms more than others.
David Tchepak
+2  A: 

You could have a very course test that goes something like this:

class ExamplesToCodeTest < Test::Unit::TestCase
  def test_convert
    # have some example markdown files in a fixtures directory
    ExamplesToCode.convert("test/fixtures/*.markdown")
    assert_equal expected_output_1, File.read("test/output/file_1.cs")
    assert_equal expected_output_2, File.read("test/output/file_2.cs")
    assert_equal expected_output_3, File.read("test/output/file_3.cs")
  end
  private
    def expected_output_1
      "... expected stuff here ..."
    end
    def expected_output_2
      "... expected stuff here ..."
    end
    def expected_output_3
      "... expected stuff here ..."
    end
end

I suppose that would make a decent integration test, but that's not what I really like, I like to have my code in bite-size chunks

First I'd create a class that can handle parsing a markdown file, e.g.:

class MarkdownReaderTest < Test::Unit::TestCase
  def test_read_code_sample_1
    reader = MarkdownReader.new
    code_sample = reader.read("fixtures/code_sample_1.markdown")
    # or maybe something like this:
    # code_sample = reader.parse(File.read("fixtures/code_sample_1.markdown"))
    # if you want the reader to just be a parser...
    assert_equal code_sample_1, code_sample
  end
  # ... repeat for other types of code samples ...
  private
    def code_sample_1
      "text of code sample 1 here..."
    end
end

Now all the code to read and parse markdown files is in the MarkdownReader class. Now if we don't want to have to actually write files you can get fancy and do some mocking with RR or Mocha or something (I'm using rr here):

class CodeSampleWriter < Test::Unit::TestCase
  include RR::Adapters::TestUnit
  def test_write_code_sample
    # assuming CodeSampleWriter class is using the File.write()...
    any_instance_of(File) do |f|
      mock(f).write(code_sample_text) { true }
    end
    writer = CodeSampleWriter.new
    writer.write(code_sample_text)
  end
  private
    def code_sample_text
      "... code sample text here ..."
    end
end

Now assuming the ExamplesToCode class uses the MarkdownReader and CodeSampleWriter classes, you can again use mock objects with RR like so:

class ExamplesToCodeTest < Test::Unit::TestCase
  include RR::Adapters::TestUnit
  def test_convert
    # mock the dir, so we don't have to have an actual dir with files...
    mock(Dir).glob("*.markdown") { markdown_file_paths }
    # mock the reader, so we don't actually read files...
    any_instance_of(MarkdownReader) do |reader|
      mock(reader).read("file1.markdown") { code_sample_1 }
      mock(reader).read("file2.markdown") { code_sample_1 }
      mock(reader).read("file3.markdown") { code_sample_1 }
    end
    # mock the writer, so we don't actually write files...
    any_instance_of(CodeSampleWriter) do |writer|
      mock(writer).write_code_sample(code_sample_1) { true }
      mock(writer).write_code_sample(code_sample_2) { true }
      mock(writer).write_code_sample(code_sample_3) { true }
    end
    # now that the mocks are mocked, it's go time!
    ExamplesToCode.new.convert("*.markdown")
  end
  private
    def markdown_file_paths
      ["file1.markdown", "file2.markdown", "file3.markdown"]
    end
    def code_sample_1; "... contents of file 1 ..."; end
    def code_sample_2; "... contents of file 2 ..."; end
    def code_sample_3; "... contents of file 3 ..."; end
end

Hopefully this gives you some ideas of how to approach testing in Ruby. Not be inflammatory, but for the most part, dependency injection is not something seen or used in the Ruby world -- it generally adds a lot of overhead. Mocking/Doubles are generally a much better option for testing.

jtanium
A: 

Interestingly enough, Derick Bailey of LosTechies.com just now posted a blog post about molding code to be more easily testable:

http://www.lostechies.com/blogs/derickbailey/archive/2010/09/10/design-and-testability.aspx

Derick mentions that in Ruby you don't have try as hard as other languages like C# to have your code be testable.

So maybe the answer is that your top-down BDD-like workflow that you picked up from J.P. Boodhoo's Nothing But .NET bootcamp http://jpboodhoo.com/training.oo doesn't apply in the same way you would in C#. Same would go for my little anagram code kata experiment I did on my blog a number of months back where I explored similar techniques in C# http://murrayon.net/2009/11/anagram-code-kata-bdd-mspec.html. I'm trying to figure out what this would mean...maybe you need to scrap the idea of the interfaces, because in Ruby you should do polymorphism via composition and not inheritance.

mkmurray
By the way, the comments on Derick's blog post are shaping up to be quite an interesting discussion.
mkmurray
+1  A: 

Out of all that pseudocode, the only thing that really worries me is "extracts code samples from file". Reading files from a directory is trivial, saving a file is trivial. Regardless of the test framework I'd spend most of my time focusing on the parsing bit.

For direct testing, I'd embed the snippets directly into the test case:

# RSPec
describe "simple snippet" do
  before(:each) do
    snippet =<<SNIPPET
increment a variable
= code
x = x + 1
SNIPPET
    @snippets = ExamplesToCode.parse(snippet)
  end
  it "should capture the snippet" do
    @snippets.should include("x = x + 1\n")
  end
  it "should ignore the comment" do
    @snippets.any? {|snip| snip =~ /increment a variable}.should be_nil
  end
end

Ah, I see another change I subtly made while writing the test: my ExamplesToCode.parse() returns an Array (or other iterable container), so that it can be tested apart from the iteration itself.

David Brady