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.
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With