It seems like just yesterday that I reported on Bill Tozier’s last article plugin. Oh, it was yesterday. Well, anyway, there I was at 0300 this morning with nothing to do, so I decided to write a test for that plugin.

It’s not just that I’m a testing fanatic. In fact you should have been asking me where the tests were for this thing. After all, doesn’t the “Agile Religion” require tests?

Damn, you’re sarcastic. Let me just sort you out for a moment. Agile is not a religion. It is a philosophy, supported by practices, some of which are about testing. And I do believe in testing, particularly if I’m going to do any refactoring. Now sit down and listen, OK?

So, as I was saying, I had in mind some improvements for Bill’s plugin. I may have mentioned yesterday that it has a weird intertwingling of model and view, although I surely did not express it so elegantly as that. So what needs to happen is that we need to pass in some kind of more highly structured argument, from which we’ll pull out the desired category, the desired output string, with some kind of tags telling us where to plug in various page values, and perhaps, who knows, a partridge in a pear tree.

So I needed tests. Should be no problem except that I had no idea how to do it.

I began by setting up a _specs folder in the rj.com folder tree, and I told Jekyll to ignore it with an entry in the config.yml. Trust me, you don’t care to see it. Then I began to write a spec, that looked something like this:

require 'rspec'
require_relative '../_plugins/latestArticle.rb'

# configuring rspec to permit both syntax flavors, at least until we decide on one
RSpec.configure do |config|
  config.expect_with :rspec do |c|
    c.syntax = [:should, :expect]
  end
end

describe "It should run" do
  it "should render based on provided context" do
    1.should eq(2)
  end
end

Once that ran, which took longer than one would hope (Pro Tip: Type rspec test_name.r not ruby test_name.rb), I began to put things into the spec to get information out. I started with LatestArticle.new, and watched it start throwing exceptions and generally whining. This told me what the parameters needed to be. Inch by inch, I learned that it wanted LatestArticle.new('latest', 'Category Foo', []), and soon enough (after all, I had the working code to look at), I learned to say something like this:

renderer = LatestArticle.new('latest', 'Category Foo', [])
result = renderer.render(the_context)
result.should eq(expected)

where the_context started null and the expected was just some string. From the exceptions that were thrown, and the messages that came out, I began by building an object called FakeContext, which wound up looking like this:

class FakeContext
  def initialize(pages)
    @pages = pages
  end

  def registers
    return self
  end

  def [](arg)
    return self
  end

  def pages
    @pages
  end

  def baseurl
    "http://ronjeffries.com"
  end
end

You can guess what I learned from these methods. First I was asked for registers, which you can see being called for in the plugin. The cute trick was just to return myself. No need for a new object. So this object is really a context but it is also a registers, whatever that is. Then pretty soon the plugin asks for pages, so I needed to devise a collection of pages. I created a FakePage object, which evolved to look like this:

class FakePage
  def initialize(answers)
    @answers = answers
  end

  def[](arg)
    @answers[arg] or "defaultFor#{arg}"
  end
end

I actually started in the method for [] just returning the thing asked for, and then writing if statements to return the right answers. A but further on, I refactored to have the answers in a Hash. So the final test looks like this:

require 'rspec'
require 'rubygems'
require 'liquid'
require_relative '../_plugins/latestArticle.rb'

class FakeContext
  def initialize(pages)
    @pages = pages
  end

  def registers
    return self
  end

  def [](arg)
    return self
  end

  def pages
    @pages
  end

  def baseurl
    "http://ronjeffries.com"
  end
end

class FakePage
  def initialize(answers)
    @answers = answers
  end

  def[](arg)
    @answers[arg] or "defaultFor#{arg}"
  end

end

# configuing rspec to permit both syntax flavors, at least until we decide on one
RSpec.configure do |config|
  config.expect_with :rspec do |c|
    c.syntax = [:should, :expect]
  end
end 

describe "latest article Liquid plugin" do
  it "should render based on provided context" do
    myAnswers = {
      'categories' => ['Category Foo'], 
      'title' => 'Some Article',
      'url' => '/somearticle.html',
      'blurb' => 'We say something good here.',
      'date'=>'2014-11-24'}
    expected = "<li><p class=\"page-category\">Category Foo</p><p class=\"page-title\"><a class=\"page-link\" href=\"http://ronjeffries.com/somearticle.html\">Some Article</a></p><p class=\"page-blurb\">We say something good here.</p><p class=\"page-link-date\">2014-11-24</p></li>\n"
    pages = [FakePage.new(myAnswers), FakePage.new(myAnswers)]
    the_context = FakeContext.new(pages)
    renderer = LatestArticle.new('latest', 'Category Foo', [])
    result = renderer.render(the_context)
    result.should eq(expected)
  end
end

So we are expecting to get queries for page['title'] and all the rest, and we return them. In the end, the test runs, and it should provide a basis for some simple refactoring.

That’s the good news.

Now for the bad news …

So then, Bill and I got together and talked about what we had learned. I had learned the above. Bill had found an example of a Block plugin. What those will do will enable you to enclose a whole bunch of text in tags saying {%my_tag%}blah blah{%endmy_tag%}. It became immediately obvious that we should build a block plugin and we set out to test-drive it in somewhat the same way that I had just done the tests for the existing tag plugin.

The basic idea was that we’d write something like this in our index page:

{%last_article_block}
  <li>
    Lovely article named {{ latest.title } for your amusement.
  </li>
{%endlast_article_block}

The trick in this example is that latest needs to be an object of class Page, populated with all the information it would normally have, and somehow saved so that when Liquid looks for latest, it finds it the same way it would blithely find page had we said {{ page.title }}

This turned out to be nearly fruitless. Partly it seemed that Bill and I were working at cross-purposes. Mostly, however, the approach of “throw an object at it and see what exceptions you get” didn’t work well. It appears that at this level, Liquid (or Jekyll, but we think Liquid) is swallowing most of the exceptions that surely get raised when you pass in nulls and dumb objects where it expects sensible ones. We learned that it wanted a Hash at some point but our instrumented Hash subset never told us what was expected. We returned ludicrous objects in response to it being called and all that happened was that Liquid peacefully went on ignoring the tags we were sending it.

One particular learning, which came later than one might have thought, was that although Liquid was using my FakeContext in the Tags mode, it was not using it in the Block mode, so our object wasn’t even getting called. Liquid has provided a context of its own, which we couldn’t instrument to find out why it didn’t contain what we wanted.

We pushed forward with this longer than we should have, because I had had some success with it moments before, and because Bill wanted to TDD it, and because our religion tells us to do it. None of these were good enough reasons, since Liquid refused to tell us what it was thinking through the rspec interface.

This is not an uncommon kind of mistake, of course. You start digging and you just keep digging. And when pairing, it can sometimes be worse, because just as I’d be ready to give up, Bill would have some hare-brained idea, and when he was stumped, I’d have one. So we dug and dug, to no avail.

A new plan

A better approach, I believe, is to run the new plugin inside Jekyll, by providing an article that calls it much as in the example above, and then make the plugin return tracing and debug information.

Note that this is not TDD. It’s more of a “characterization”, not even a “characterization test”, because for now, we’re not trying to lock behavior in as one would with a characterization test, we’re just trying to find out what the behavior is.

That’s what I’ll try next, and I’ll report in the next article. That might not be today.