Writing a Typo Sidebar Test First In Rails

Writing the Sidebar

More and more of my traffic (thanks to the wonderful stats tracking of both Mint and Google Analytics is coming from search engines.

So, in an attempt to try and keep visitors hanging around for longer (and to make it easier to see related stuff), I’d like to add a related articles Sidebar component.

For example, if users are on a page that mentions Domain Driven Design, I’d like them to see other articles that also cover Domain Driven Design.

And since Typo already uses tags for describing content, we can re-use the same mechanism. So, for any tags an article has, we can show other articles that share those same tags.

So let’s try writing it more as an XP story to focus our effort. How about…

The Story


As a blog owner, I’d like to make it easier for visitors to find content and related posts. I’d like to see a list of related articles appear in the sidebar when I view an article. I’d like relationships to be indicated through sharing tags.

Writing the Code

This post covers notes from when I was writing the Sidebar, and should cover my thought processes at the time pretty well. I’m definitely learning Ruby a whole lot still, so it was a great exercise for me to learn through. I’ve found also found Test Driven Development a great technique to pick up a new language and API etc. on my first ThoughtWorks project – a Java based banking project.

For anybody who isn’t familiar with Test Driven Development, or working with developer testing, I’d recommend this particularly great podcast from a talk Kent Beck gave around developer testing.

A lot of the tests make use of articles and tags that have been added to the fixtures. I won’t cover these here, but the changes are all inside the Subversion patch file.

The First Test

We want to know that we can find related articles so let’s get stuck in and write some of the unit testing code first. Once we have a list of related articles, then we can figure out how to display them. So inside our `article_test.rb` file let’s add the following




123456
def test_can_find_related_article  first_article = contents(:related_article_1)  related_article = contents(:related_article_2)      assert related_article.related_articles.include?(first_article)end

We’re using the existing `contents.yml` fixture to add some test articles (notably, `:related_article`). I won’t detail the changes here, since they’re mostly quite obvious. Full changes are available in the patch file should you want to see them.

The easiest way to make this test pass (and check our test is working correctly) is to return all articles.




123
def related_articles  Article.find_allend

Great, it passed. But, we only want to display related articles if we do actually have related articles. So, let’s add a test that makes sure that if we have an article with no tags (and therefore no relationship to other articles) we have an empty array.




123
def test_returns_no_related_articles  assert_equal 0, contents(:article3).related_articles.lengthend

If we run the test we’ll see it fail since we’re currently returning all articles. So, let’s test that we can restrict the articles returned to be those just matched by the tag. We can do that by changing our `related_articles` method




12345
def related_articles  articles = []  articles = Article.find_all unless self.tags.length > 0  articlesend

Run again and we’re good to go. Let’s now write a test to ensure that our related articles only includes those which share the same tag and that the first item is our related article (so we don’t find ourselves in the results etc.):




1234567
def test_can_find_only_related_article_with_same_tag  first_article = contents(:related_article_1)  related_article = contents(:related_article_2)      assert_equal 1, first_article.related_articles.size  assert_equal related_article, first_article.related_articles.firstend

If we run the test, we should see it fail since we have too many articles being returned.

1) Failure:
test_can_find_only_related_article_with_same_tag(TagTest) [test/unit/tag_test.rb:80]:
expected but was
.

Excellent. So now we need to write a proper finder method, and as with many things, there a couple of ways we could go forward.

One way, would be to write some custom SQL to do the querying in one hit, but I feel that that would probably take a little longer (and be a little more complex – and unclear) than just navigating the objects and their relationships. So, instead, we’ll write it as part of our `Article`.




1234567
def related_articles  related = []      self.tags.each do |tag|    related.concat tag.articles  end  related - [self]end

If we re-run our test we should see it pass. It’s not the prettiest code, but it works.

Finally, we also want to ensure that we only link to articles that are published, so we’ll need to remove any which haven’t been. Again, let’s write a test




12345678
def test_related_articles_only_includes_published_articles  first_article = contents(:related_article_1)  related_article = contents(:related_article_2)  related_but_unpublished_article = contents(:article4)      assert_equal 1, first_article.related_articles.size  assert !first_article.related_articles.include?(related_but_unpublished_article)end

Let’s run the test and see that it fails because we need the additional `:related_article_6` entry in our fixture. After we add that we should see it fail with too many entries.

So, again, focusing on adding the simplest thing that’ll work, let’s add the code to filter out unpublished articles.




123456789
def related_articles  related = []      result = []  self.tags.each do |tag|    related.concat tag.articles  end  related.collect {|a| result << a if a.published && a != self}  related - [self]end

If we run the test again now we should see it pass successfully. Of course, the code is pretty unattractive and un-Ruby like! So, let’s spend a little time tidying it up before we move on. How about…




1234567
def related_articles  related = []  self.tags.each do |tag|    tag.articles.collect {|a| related << a if a.published && a != self }  end  relatedend

That looks much tidier, and the tests still pass so we know it works. I’m happy with that for the time being, so let’s move on to the next part.

The Sidebar Component

We can now retrieve all articles that share the same tag. Rather than spend more time improving the algorithm for finding related articles, instead we’ll focus on delivering this simpler functionality into the hands of our customer sooner. This is what is often referred to as delivering ‘Vertical’ value: delivering a more complete but thinner story. A lot of what 37signals refer to in their Getting Real book has tie-ins to this kind of approach, focusing on simpler now, rather than waiting for everything.

So, the next step then is to write the Typo sidebar component that’s going to use our list of related articles in a list.

Now there doesn’t seem to be a great deal of content about writing tests for components in rails (read: nothing). But, since they’re supposed to be analogous to partials, but instead for controllers, it seems sensible to place them with our other functional tests. At least for the time being.

So, create a `related_articles_controller_test.rb` in the functional tests directory and let’s set about writing our first test.

The first thing we need to do is make sure we include the test helper (like all other functional tests). We also need to include our related articles controller (that doesn’t exist yet) so we can use it in our test. So, at the top of the file add the following




12
require File.dirname(__FILE__) + '/../test_helper'require File.dirname(__FILE__) + '/../../components/plugins/sidebars/related_articles_controller'

We also want to add all the setup stuff so we can use our controller inside our various tests, so let’s also add that




1234
def setup  @controller = Plugins::Sidebars::RelatedArticlesController.new  @request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new    end

Now, on to our first Sidebar functional test.

The First Sidebar Test

We want to know that we can see the related links when we view one of our previously created articles (we’ll re-use the related articles stuff in the fixtures we created earlier for our unit tests).




123456
def test_when_we_have_an_article_we_can_see_related_items  get :content, :contents => [contents(:related_article_1)]      assert_response :success  assert_tag :tag => 'ul', :children => {:count => 2, :only => {:tag => 'li'}}end

Typo uses a `contents` array inside Rails’ `params` object to pass the items we’re viewing around. So, we’ll pass in one of our articles as that contents. We then go on to test that our controller renders successfully, and that we can a list with two items.

If we run the test, we’ll see that we haven’t created the sidebar component source file yet so let’s create the structure for our component. Add a `related_articles` directory under the `components/plugins/sidebars` directory. Then, inside that directory add `content.rhtml`, and finally, add `related_articles_controller.rb` inside the `components/plugins/sidebars` directory.

Once that’s been done, let’s add the various sidebar stub stuff (as well as our `content` action that we’re `GET`ting in our test:




1234567
class Plugins::Sidebars::RelatedArticlesController < Sidebars::ComponentPlugin  description "Displays articles that share the same tag."  def content    #do stuff  endend

If we run the test, we’ll see that we don’t find any lists, so we need to edit our view and wire it up.

So, we’re going to need our `RelatedArticlesController` to give us an array (`articles`) that’ll contain the links we want to show. If we run the test now, our `assert_response :success` will fail since our view can’t find what it needs. The simplest way to fix that is to have the following in our controller




123
def content  @articles = []end

We’ll just use an empty array for the time being. That’ll then mean our second assertion will fail since we also need to see two items (for our article).

So, we’ll add the code to do that.




123
def content  @articles = params[:contents].first.related_articlesend

If we run our test, we’ll see it passes. Onto the next test!

Now, seeing a list is all very well, but we need to be able to click links within that list that’ll take us to the various articles (since we’re trying to drive up the number of views people make when visiting).




1234567
def test_we_can_see_article_links  article = contents(:related_article_2)  get :content, :contents => [article]      assert_tag :tag => 'li', :children => {:only => {:tag => 'a'}}  assert_tag :tag => 'a', :attributes => {:href => article.location}, :content => article.titleend

Firstly, we’ll check our list items have anchor tags to display the links, and then check those links have the correct address in them.

If we run the test, you should see the message ”`no tag found matching :attributes=>{:href=>”/articles/2004/06/01/article-4”}, :tag=>”a”, :content=>”Article 4!”`” appear.

Typo has some helpers for writing links to various bits of content, so we can re-use those to actually generate the tag. So, inside our `content.rhtml` view, add

If we re-run we should see our test pass.

Since Typo uses the `contents` array in the `params` object to pass articles around for all pages we’ll need to check that we only see our sidebar when we’re viewing an article, and not viewing the home page (for instance).




12345678910
def test_when_viewing_home_page_no_related_articles_shown  get :content,    :contents => [      contents(:related_articles_1),      contents(:related_articles_2)    ]      assert_response :success  assert_no_tag :tag => 'ul'end

If we re-run we’ll see our test fail because we can still see our list. So, we’ll need to add some logic to our controller to only provide an articles list if we’re viewing an individual article, otherwise we’ll just leave it with an empty array.




1234
def content  @articles = []  @articles = params[:contents].first.related_articles unless params[:contents].to_a.size > 1end

If we run our tests again we’ll see it pass. Now, that code’s pretty ugly so let’s try to refactor it a bit now that we have some tests to make sure we don’t break anything.

Firstly, let’s try and extract out how we get the current article we’re viewing from the `params` object as follows




123456789
def content  @articles = []  @articles = article.related_articles unless params[:contents].to_a.size > 1endprivatedef article  params[:contents].firstend

Then, let’s run our tests and make sure everything is still passing. Excellent, now it’s not immediately clear why we’re checking the length of `params[:contents]` (other than we don’t fill articles). So, let’s make it a little clearer




1234
def content  @articles = []  @articles = article.related_articles if viewing_article?end

and add another private method




123
def viewing_article?  params[:contents].to_a.size == 1end

Much better. As a little safety net, we’ll also make sure that we don’t consider ourselves to be viewing an article unless the first item is also an article. So, let’s add




123456
def test_unless_first_item_is_an_article_display_nothing  get :content, :contents => [tags(:foo_tag)]      assert_response :success  assert_no_tag :tag => 'ul'end

If we run the test we’ll get a 500 error because we’re assuming that if we have one item in our array it’s an article and that we can get related articles. So, let’s add the code to only consider ourselves as viewing an article when the first item is indeed an article.




123
def viewing_article?  params[:contents].to_a.size == 1 && params[:contents].first.kind_of?(Article)end

If we re-run the tests we’ll see it all run successfully.

Finally, if we don’t have any related articles, we’d prefer it if we didn’t see anything in the sidebar, rather than us having an empty list. Our test:




123456
def test_if_no_related_articles_found_nothing_is_displayed  get :index, :contents => [contents(:article3)], :sidebar => @sidebar      assert_response :success  assert_no_tag :tag => 'div'end

If we run the test, we’ll fail because we’ll still have our `div` containing an empty list. All we need to do is wrap our `div` within the following

0 %>...

Re-run the test, watch it pass, and we can proceed onto the Sidebar configuration aspect of our code.

Sidebar Configuration

The advantage of Typo’s Sidebars are that they allow you to configure them through the administration interface.

We want to be able to configure the maximum number of articles that are shown in the related articles list, and the title that will appear above the list.

Our first test is that our configuration includes a Title, that will appear above the list.




1234
def test_can_configure_title_in_admin  assert_equal "title", Plugins::Sidebars::RelatedArticlesController.fields.first.key  assert_equal "Related Articles", Plugins::Sidebars::RelatedArticlesController.default_config["title"]end

We’re testing that we have a configurable title, and that the default value is “Related Articles”. As usual, run the tests and watch them fail. Onto the implementation (inside our `RelatedArticlesController`):




12
class Plugins::Sidebars::RelatedArticlesController < Sidebars::ComponentPlugin  setting :title, 'Related Articles'

Re-run the tests and we can watch them pass.

Now we want to make sure we can see that title above our list, so let’s write a test that we find an `h3` element with our title in it




123456
def test_can_see_default_title  get :content, :contents => [contents(:related_article_3)]      assert_response :success  assert_tag :tag => "h3", :content => "Related Articles"end

At the moment, the test will fail because our template hasn’t yet had the code added. So, in the interest of getting it passing quickly, let’s do the following:

Related Articles

...        
Our test will now pass, so we know that we’re finding the element correctly. Now let’s proceed to write another test that will break our shortsighted implementation. Here we’ll be using the `index` action within our controller, and providing it with Sidebar configuration. Note that this is different for our previous tests, since this is the first time we’ve needed to test this kind of code. At this point (when I was working) I changed the previous test code to match this.


  123456789
  def test_can_see_configured_title  sidebar = Sidebar.new()  sidebar.config = {:title => 'My Articles'}  get :index, :contents => [contents(:related_article_3)], :sidebar => sidebar    assert_response :success  assert_tag :tag => "h3", :content => "My Articles"end


        
Here we’re also passing through our `:sidebar`, this is the representation of the item’s configuration. So, when we render our component our controller can pull the `title` `setting`. So, let’s add the code inside our view template



...        
If we run the test, we’ll get a failure since our title configuration setting doesn’t exist on our Sidebar component yet. All Sidebars are configured the same way, and nicely (thanks to some refactoring from Piers Cawley a while back)
        
All we need to do is add a setting as follows


  12
  class Plugins::Sidebars::RelatedArticlesController < Sidebars::ComponentPlugin  setting :title, 'Related Articles'


        
If we re-run the test all should pass, and we can write our second configuration test – that we are able to set the number of items we’d like to see in our list. We also want to have a label for our field also – this will appear next to the textbox to indicate what the setting controls.


  12345
  def test_can_configure_number_of_items_in_admin  assert_equal 'count', Plugins::Sidebars::RelatedArticlesController.fields.last.key  assert_equal 5, Plugins::Sidebars::RelatedArticlesController.default_config['count']  assert_equal 'Number of Articles', Plugins::Sidebars::RelatedArticlesController.fields.last.options[:label]end


        
If we run our test, we’ll see a failure because we only have one field
        
<"count"> expected but was<"title">
        
So, let’s add a setting to fix the first assertion


  123
  class Plugins::Sidebars::RelatedArticlesController < Sidebars::ComponentPlugin  setting :title, 'Related Articles'  setting :count, 5


        
If we re-run, we’ll have a failure because our configuration field’s options doesn’t contain a label. So, let’s add that to our setting definition in our controller


  
  setting :count, 5, :label => 'Number of Articles'


        
If we re-run our test, we’ll see it pass!
        
Finally, we need to write the tests to ensure that we limit the number of items we actually see to the number that’s specified in the configuration. Let’s take our example from an earlier test. This time, we’ll say we only want to see one item though (rather than the two that are actually associated):


  123456
  def test_can_see_limited_number_of_items  get :index, :contents => [contents(:related_article_3)], :sidebar => @sidebar      assert_response :success  assert_tag :tag => 'ul', :children => {:count => 1, :only => {:tag => 'li'}}end


        
To do this, we’ll need to add a limiter to our `related_articles` method that can limit the number of results. Let’s use a similar approach to elsewhere, so we want to be able to call `article.related_articles(:limit => 1)` (for example).
        
So, let’s add the code to our article’s test


  1234567
  def test_can_find_a_limited_result_set  article = contents(:related_article_3)  first_related_article = contents(:related_article_4)  second_related_article = contents(:article2)      assert_equal 1, article.related_articles(:limit => 1).sizeend


        
If we run the test now we’ll get an exception for not having enough parameters. So let’s add the parameter and run the test again


  12
  def related_articles(*params)  related = []


        
We’ll get a test failure (which is good – we now need to implement the restriction).


  1234567891011121314
  def related_articles(*options)  related = []  self.tags.each do |tag|    tag.articles.collect {|a| related << a if a.published && a != self }  end      if options.length == 0    related  else    params = options[0]    count = params[:limit] - 1    related[0..count]  endend


        
If we run the test again now, we’ll see it pass. It works by converting the parameters to the method into an array. We then pull down the first of the parameters (assumed to be a hash of arguments), and we then retrieve a limit from the hash.
        
Next, we want to make sure that if we specify a limit which is larger than the items we have, we return all items anyway


  1234567
  def test_retrieves_all_items_when_specifying_an_out_of_range_index  article = contents(:related_article_3)  first_related_article = contents(:related_article_4)  second_related_article = contents(:article2)     assert_equal 2, article.related_articles(:limit => 5).sizeend


Brilliant. All tests passing, we can fire up the application, add our sidebar, and enjoy the result :)

Iteration Retrospective        Well, it’s been an interesting experience. Not only because this is my first real dig around Typo’s code (which seems to be in a state of flux right now – but it’s great to see it’s getting attention!), and my first play around with Ruby code I’ve not written (which I’m sure is almost always not as great as it could be).

        
It took me a little while to figure out how to write and test the code (largely because of my unfamiliarity) with the Typo Sidebar code (and Typo in general). And the Sidebar seemed a little too complicated for my liking (I may try and refactor some of the code when I feel a little more confident in my understanding), but once again Rails’ functional testing made me so much more confident writing code.
        
What really didn’t help was not being able to find any tests for the various Sidebar components, so I had really no idea how to go about doing it! But writing code test first really made it easier for me to focus, and made me more confident the code I was writing was working. And, that should any future Typo changes be made, I know that my Sidebar is less likely to break unbeknownst to me! Piers has also commented that he is considering changing the way the Sidebars work. I’m definitely intrigued by his suggestions from what I’ve seen, and may even give some refactoring a go myself and see where I end up. You gotta love evolutionary design :)
        
I’m also not sure whether I like Rails’ definition of unit and functional testing, but I appreciate what it’s trying to achieve. I’m also still undecided as to whether I prefer having test object setup done inside fixtures versus inside the tests themselves. I had to do lots of to-ing and fro-ing between my unit test, `contents.yml`, `tags.yml` and `articles_tags.yml`. That context switching became expensive, especially during late night hotel hacking when I started to get tired and lose focus.
        
Not only that, but a lot of Typo’s tests rely on it having a predefined number of published articles (in an assumed order), by adding new articles for the new tests, it broke some of the existing tests which meant investigating that the number was indeed meant to change. I’m not too sure of the best way forward in this situation.

Where to go from here        I’m posting this article, hopefully to give people ideas about how they can go about extending existing open source Rails code, and hopefully that people will take a look at my code and make suggestions/comments about it. I’m especially excited by the prospect of people getting my code from the public Subversion repository, and submitting patches in Trac.

        
It’s this process of constantly learning from other people (whilst pairing both with other ThoughtWorkers and client developers) that makes it such a brilliant place to learn (and have fun).
        
I’m definitely going to improve the way articles are considered to be related, hopefully ordering the list based on the number of shared tags. I’m also going to set to work on writing a plugin right away to make it easy for other Typo users to get this functionality into their install without needing to update their whole application.
        
Once I’ve made a few improvements (I’d also like to make sure the persistence store access code is a little better at not having to retrieve too much data in one go) I’ll be sure to make a ticket on Typosphere and see if I can get it into the trunk for the future, if that’s where the project wants these kinds of things to go.
        
I’ll work on getting a better packaged component together as soon as possible, look out for future posts!