Testing Your Templates, and Why It Doesn't Always Work

| 6 Comments

Sometimes you find bugs in the most surprising places.

I've long used test-driven design for code I care about. Knowing that it works, works reliably, and will continue to work helps me write the right thing. As I've gained experience with testing, I've come to appreciate the nuances of how and what to test and why. For example, I often don't bother to test exploratory code—it's more important for me to learn how code might work to help me figure out how to design the real thing than it is to hew to a rigid mindset of "You must always test everything."

Similarly, I see little value in rigorously testing basic accessors, and thank goodness for Moose for turning class declarations into declarative code.

In more serious discussions of testability and testing, people often argue that testing UIs is impossible, or merely difficult, or rarely worth the trouble to do so completely. I agree to some respect; the most effective way I've seen to test UIs is to test the backend data model rigorously, write a very thin controller to marshall and unmarshall requests and responses, then to write a handful of tests which explore the edge cases of UI interactions. That requires a deep understanding of what happens for each interaction, and you can only test those interactions which produce measurable side effects.

Let that latter point sink in for a moment.

My most recent work on multiple projects includes a lot of embarrassingly parallel batch processing work, where the user interface is largely static, produced daily from a series of templates. Generally the templates are correct if they pass the eyeball test with live data.

This works except when it doesn't.

Consider a snippet of Template Toolkit code to display a list of stories:

[% FOR e IN entries %]
    <div class="story story[% loop.count %]">
        <h3><a href="[% e.url %]">[% e.title %]</a></h3>
        <p>[% IF e.author %]
            [% e.author | html %]<br />
        [% END %]
        [% f.format( e.posted_time ) %]</p>
        [% IF e.has_image %]
            <img src="[% e.image.full_file %]" alt="[% e.title | html %] image" class="image" />
        [% END %]
        <p>[% e.short_content | html %] <a href="[% e.url %]">Read More</a></p>
    </div>
[% END %]

That code had a bug. Can you find it by eyeballing it? Neither could I, for a long time. I'll give you a hint: it never displayed images associated with stories. Give up?

I wrote backend test after backend test, fixing a couple of other bugs along the way, then traced through the code in a debugging batch until I had convinced myself that the data model was completely correct. The bug had to be in the template because it could be nowhere else.

Rigorous template tests didn't seem to help either, and I'd almost convinced myself that I had a nasty spooky action at a distance bug somewhere in the data model before I saw the problem:

[% IF e.has_image %]
    <img src="[% e.image.full_file %]" alt="[% e.title | html %] image" class="image" />
[% END %]

When I added a test for the has_image() method on story objects to the data model tests, it failed immediately because that method did not exist. It used to exist, but a refactoring somewhere went too far and removed that method and its tests from everything except the template. Because Template Toolkit is not so strict about the distinction between method and property access, the lack of a method is no big deal. It fell back to property access on a hash and, finding no such value in the hash, returned a false value.

There are ways to fix this. I'm sure there's a plugin for Template Toolkit, and probably a debugging/diagnostic mechanism somewhere I haven't yet discovered. Yet the problem remains.

I've heard it said (Rich Hickey? Martin Odersky? I think it was Rich.) that every bug in your program has passed the type checker. That statement contains wisdom. Yet it's also true that every bug in your program has passed the test suite. (We assume you run with type checks and keep your test suite fully passing.)

Neither is a substitute for careful thought and understanding. Both protect you only as far as you can take advantage of them.

My next post will explore some options for making these kinds of errors easier to catch way before the point of weird debugging sessions.

6 Comments

This is one of a few fundamentally broken design decisions in TT that drive me out of my mind when I have to use it. Obscuring the distinction between hash keys and methods is incredibly dangerous.

I wrote about this and some other issues with TT ages ago on my use perl journal (http://use.perl.org/~autarch/journal/33175).

I'm now using Lift on a project, and it does templates in a way that I really like. In Lift, the template is HTML5, but with CSS used to hook into "snippets", which are Lift components. So the template might contain ..., which would invoke the "balance" method on a class (or maybe instance) "Ledger". This method takes a node tree for the elements inside the div, and essentially rewrites it. There are no conditions, loops, or anything, although you can write them yourself if you need to.

The great thing about this as that each snippet method can be tested outside a view, with mocking and everything. This is a huge step forward for testability, and virtually all web frameworks I have seen really don't get to grips with view testing. I'm currently thinking whether this approach can be fitted into one of the Perl web frameworks somehow. Of course the difference is that the template isn't text, it's HTML nodes, and a lot of Scala sugar is used to make the rewrites elegant, but as an approach it works very well for testability.

And of course I got bitten trying to use HTML as content. The typical "designer friendly" snippet invocation was supposed to look like:

<div class="lift:Ledger.balance">...</div>

I think this is a strong example for "logic and in fact executable code does not belong into your template". I'd think that with HTML::Zoom this kind of problem would've been avoided altogether. Meanwhile Text::Xslate is a good compromise.

How would you rewrite this template to avoid the conditional? Am I misunderstanding--are you suggesting that the template language should not allow method calls on objects?

If so, I think that pushes logic too far out of templates.

I was talking in more general terms. Factually i'd say that if you're pushing anything to the template render that is not a simple hash/array/scalar structure you're doing something wrong; similarly, if you're using anything beyond GET/IF/LOOP instructions in the template, then you have too much logic in there as well. Text::Xslate helps because it has more useful error messages and can tell you about undefined values without getting all crazy, and because it doesn't support some of the more outlandish parts of TT.

HTML::Zoom takes an entirely different approach: You hand it a normal piece of HTML and then instruct it via CSS selectors to fill in, or repeat or cut out certain parts of the template. All of this happens within the Perl code, so you end up with templates that are completely testable.

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

affiliated with ModernPerl.net

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on January 17, 2012 11:29 AM.

Modern Perl 2011-2012 Edition Released! was the previous entry in this blog.

Imagine if caller() Returned Stack-Capturing Objects is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Sponsored by Blender Recipe Reviews and the Trendshare how to invest guide

Powered by the Perl programming language

what is programming?