May 10th, 2013

Developing Gems with TDD and Minitest: Part 2

In part 1 we set up our gem and a few dependencies and got our testing environment ready as well. Now it’s time to actually start writing our gem. We’ll start off with our Post class which when given a path will read the file, parse the YAML Front Matter and render our Markdown content.

First we need to create the file lib/mark/post.rb to contain our post class.

module Mark
  class Post
  end
end

We also need to create a matching test file, test/post_test.rb.

require 'test_helper'

class PostTest < MiniTest::Unit::TestCase
end

Require our new Post class and dependencies in lib/mark.rb.

require 'yaml'
require 'tilt'
require 'redcarpet'
require 'fileutils'
require 'date'
require 'mark/version'
require 'mark/post'

Lastly we need to create a fixture so our Post class can work with real content for us to test on. Create the test/fixtures directory and a file inside of that directory called valid_post.md.

---
  title: Valid Test Post
  date: 02/27/1992
---

# Valid Test Content

This is what our post class expects to be in the file that we will read. It starts with YAML Front Matter which is just YAML wrapped inside of --- at the top of the file. The rest of the file is a standard Markdown document that we will parse with Redcarpet using Tilt.

Before we write any implementation code, lets go over the specifics of what we want our Post class to do, write the basic structure that our implementation will follow, then write our tests.

How our Post class should behave:

  • Parse meta from YAML Front Matter as a hash
  • Parse date from meta into a Date object.
  • Render Markdown into content

Now that we know what we want our Post class to do we can write the structure of our app. Based on what we have so far a good starting point would be to read the file into a class variable, then call a method to parse the content.

module Mark
  class Post
    attr_reader :meta, :content

    def initialize(path)
      @file_contents = File.read(path)
      parse
    end

    private
    def parse
    end
  end
end

Again, all we do is read the file contents into a class variable and then call our (currently empty) parse method. Since we have a decent starting structure we can write our tests.

require 'test_helper'

class PostTest < MiniTest::Unit::TestCase
  def setup
    @post = Mark::Post.new(File.expand_path('../fixtures/valid_post.md', __FILE__))
  end

  def test_meta_returns_hash
    assert_instance_of Hash, @post.meta
  end

  def test_meta_parses_title
    assert_equal 'Valid Test Post', @post.meta['title']
  end

  def test_meta_parses_date
    assert_equal Date.new(1992, 2, 27), @post.meta['date']
  end

  def test_content_valid
    assert_equal "<h1>Valid Test Content</h1>\n", @post.content
  end
end

If you aren’t familiar with testing or Minitest, we can define a method called setup which Minitest will run before each of our tests. This allows us to create a fresh instance of Post passing it the path to the fixture we created earlier. Then we just test to make sure our parse method returns the correct meta values and parsed Markdown.

Now when you type rake test you should get E’s and F’s which is exactly what we want. Since we have our tests in place we can start writing our implementation.

def parse
  split_content = @file_contents.split(/^---/, 3)
  split_content.shift

  @meta = YAML::load split_content.shift
  @meta['date'] = Date.strptime(@meta['date'], '%m/%d/%Y')

  @content = Tilt::RedcarpetTemplate.new {split_content.shift}.render
end

Our implementation is very simple and does just enough to make the tests pass and nothing more. We start off by splitting our content into 3 separate sections, an empty section, the YAML content that was between the --- pair, and our Markdown content. Since the first element of our split content is empty we just call shift to remove it.

We then call YAML::load on the second element of our split comment by using shift to remove and return it. This loads our front matter into a hash of values but they’re all strings. Since loading our YAML returns strings we parse the date string into a Date object and replace the old string representation.

Finally we create a new instance of Tilt::RedcarpetTemplate and pass it a block with the final part of our split content and tell it to render. We assign this to our content instance variable which should complete our implementation.

Now when we run our tests we should have four .’s which means our tests are passing and our code should be doing what we specified earlier.