Developing Gems with TDD and Minitest: Part 3

Part 2 left off with our project having a post class that is able to parse YAML Front Matter from our Markdown files, and then process the Markdown as well. Now we need a way to have templates for our Posts as well as writing the files to our site directory.

The Template

First, we need to create the file lib/mark/template.rb to hold our class and the matching test file at test/template_test.rb.

For our template class we want to instantiate it with a path to our application's template and give it a write method that accepts the content that we want to use inside the template and a path to where we want to write the file.

module Mark
  class Template
    def initialize(path)
      @file_contents = File.read(path)
      parse
    end

    private
    def parse
      @unrendered = Tilt::ERBTemplate.new { @file_contents }
    end
  end
end

Our initializer is simple, we just pass it a path and read the file at that path and then call the private method pase. We don't do any error checking right now, but that is definitely something we should handle later. Our parse method is also very basic, we just create our @unrendered class variable to be an unrendered Tilt template.

We also need to write our write method, which takes two arguments. The first argument is the content that will be rendered inside of our template, and the second argument is the path to write the file at.

module Mark
  class Template
    def initialize(path)
      @file_contents = File.read(path)
      parse
    end

    def write(content, path)
      file = File.open(path, 'w')
      file.write(@unrendered.render { content })
    end

    private
    def parse
      @unrendered = Tilt::ERBTemplate.new { @file_contents }
    end
  end
end

Our write method is pretty basic, open a writeable file at the path we pass it and then write our rendered content to the file. Where it gets interesting is how we write our tests for this class. This is where we start using mocks and stubs. If you don't know what mocks and stubs are, mocks are fake objects that test that certain methods have been called on it, and stubs "fake" methods and return what you specify it.

We're using the built in mock and stub features of Minitest. Minitest mocks and stubs aren't very powerful like some other alternatives such as Mocha or rspec-mocks.

Before we get started writing our template tests, create the file test/fixtures/valid_template.rb. This will be our basic template so we can test if our write method writes the correct content.

<html>
  <%= yield %>
</html>
require 'test_helper'

class TemplateTest < MiniTest::Unit::TestCase
  def setup
    @template_path = File.expand_path('../fixtures/valid_template.erb', __FILE__)
  end

  def test_content_writes_correct_content
    file_path = File.expand_path('../fixtures/', __FILE__) + '/test.html'
    file = MiniTest::Mock.new
    file.expect :write, nil, ["<html>\n  hi\n</html>\n"]

    File.stub :open, file do
      template = Mark::Template.new(@template_path)
      template.write 'hi', file_path
    end

    file.verify
  end
end

We only have one test here because we only have one public method, and it's a fairly simple test. First we assign file_path to where we want to write our file. We also create a mock, then expect that mock to have the write method called on it by the time we call verify on it.

Then we get to our stub, we stub out File.open and make it always return our file mock while inside the block we pass to it. Inside of that block we create a new instance of our template class and pass the string 'hi' to it. Finally we hit file.verify which ensures that our mock had the write method called on it, returned nil, and was passed the arguments inside of the array.

We don't use our Post class in this test because it and our Template class are separated quite a bit. If we decided to create and use an instance of Post we'd have the possibility of failing this test for no reason if the Post. If you wanted to test the two working together, you should probably write an integration test.

Now we have our Post class and our Template class. Since our template class can write content now, all we need is some way to tie these two together. In the next post we'll work on making a class that lets us use these classes together to write out posts.