Test Driven Ajax (on Rails)

Phlip

April 9, 2007

Abstract

The World Wide Web has come a long way from static HTML pages. Today's developers enforce and enjoy standards, and we have built the web's primitive tools into advanced libraries, frameworks, and platforms.

With these new freedoms come new responsibilities. Developers can now write some amazing bugs. A bug in a web page, hosted in a free web browser, can render expensive servers useless. Modern editors help rapidly write tangled and crufty code, the perfect habitat for bugs of every species, in situations that are hard to debug. We need help from the mortal enemy of the bug: Test-First Programming.

This Short Cut seeks fixes for the hardest situation in web development; proactive test cases for Ajax code. We survey existing techniques, and invent new ones. Our goal is heads-down programming, with-out repeatedly clicking on a web browser.


This Short Cut explores Test-Driven Development techniques for Ajax, using Ruby on Rails. We start with the Rails built-in test system, extend our focus to cover more Ajax, and then use Ajax to generate an acceptance test framework for web sites.

Prerequisites

Get ready to employ all the usual suspects:

Note

Ajax Builder::XmlMarkup CGI CSS ERb HTML HTTP JavaScript JSON Perl Prototype.js Rails Regexp REXML RJS Ruby Wiki XML XPath YAML(Syck) YPath

If any large term is unfamiliar, you should bookmark a tutorial for it. Rails makes all those systems easy to work with, and we will use them in limited but sometimes unfamiliar ways.

Those among you with more experience can follow along using Rails or your own web platform. All the experiments rely, ultimately, on dynamic HTML and modern JavaScript.

Students of web development should learn Rails to follow this Short Cut (and beyond). You'll find that much easier than translating all these unfamiliar ideas into a more familiar platform. Ruby on Rails is very easy to install and learn, and its central modules are thoroughly documented. I started with the Pragmatic Programmers' Agile Web Development with Rails. (The word "agile" in the name means, among other things, that the book illustrates many test systems. But it can't cover all kinds!)

Web Testing Challenges

Unit tests should be light and fast, and Ajax raises some special problems. It only works inside a real web browser interacting with a real web server. Both are heavy and slow. Although tests often use mock objects to avoid the overhead of real objects, no test should mock all of JavaScript, Document Object Model, and HTTP. Such mocks would simply tell our tests what they want to hear.

This Short Cut uses Rails and Ajax to build a web site that works as a test runner, targeting any kind of old-school web site, and targeting any site that uses the prototype.js library for Ajax. In other words, we fight fire with fire.

Some of your projects might already have too much fire. This Short Cut also surveys many traditional techniques to test Ajax. We start simply, with the Rails default testing system.

When you generate a Rails project (start at http://www.rubyonrails.org/down to learn how), you get a test/ folder crammed with exemplary systems to write test cases. Each system matches corresponding elements in the Rails framework. Unlike some legacy systems, Rails provides no excuse to avoid test-driven development. Your clients will enjoy the rapid development and low bug rate.

Jargon Alert

The Rails term fixture means fixed database records, ready for tests to play with. The original Test-Driven Development community called those records resources, and called reusable test-side functions fixtures. This Short Cut defers to Rails and calls reusable test-side methods services.

This Short Cut shows how to write test cases that grow a project. It illustrates our project's architecture by showing where it came from.

Rails helps us write tests that constrain our data models and controllers. Ajax, however, is a different beast. It uses complex JavaScript to interact in subtle ways with any number of web browsers. Unlike Rails, JavaScript and DOM were not designed for testing.

What's the Deal with Ajax?

Rails pages perform Ajax by building strings of JavaScript and pushing them into HTML pages. The JavaScript calls into Script.aculo.us libraries, starting with prototype.js. A web browser responds by exchanging messages with its web server and updating page elements.

The miracle of Ajax is that these messages refresh one part of a web page without repainting all other parts. This allows much leaner usability. If you want to change something, just click on it. It changes instantly, because it is decoupled from everything else. You don't need to find a reason to repaint everything. The main problem with Ajax is that every browser behaves just a little differently.

In an Enterprise situation, where everyone uses the exact same Starfleet Web Browser, platforms like Rails make Ajax competitive with any of the aggressively marketed desktop GUIs, such as .NET Forms or Swing. On the open Internet, a light touch of Ajax can rescue a proficient user from clumsy operation.

To keep the bugs out of these situations, we need test cases on Ajax. Our assertions have two odious choices. They can read the JavaScript that Rails generates and spot-check its details, or they can use special test runners that only work with one brand of web browser. These slowly send pages into the browser, and then detect what it did with them.

This Short Cut pushes our test-case options far beyond the current state of the art. We will parse JavaScript as a language, and then test its behavior on any browser.

What's Test-First Programming?

When programmers write failing tests, and then write code to pass the tests, they produce robust code that strongly resists bugs. This is the practice of Test-First Programming. It can be hard to do, because we don't always know what tests to write. We might run the tests and observe the code works but the test fails. In those cases we can edit the test to "help" it pass, but that's not ideal; we should know enough about the production code to be able to predict the best assertions accurately. We should work on the test case until it fails for the correct reason, and then upgrade the code until the test passes.

The ultimate goal here is Test-Driven Development. This methodology expresses all aspects of software engineering as automated tests. Customers request features by describing "customer tests," and developers implement by generating "developer tests." Programmers put all these test cases into a huge batch, and run some of them each time they change the code. They run all of them before integrating or releasing. Under TDD, project details at all scales, from long-term goals to tiny details of production code, are all specified, analyzed, reviewed, and enforced as proactive test cases. Teams use tests to communicate specifications, to rapidly design and implement, and to make life very hard for bugs.

A test case is one method of a test suite, following this pattern:

def test_foo
    foo = assemble_foo()
    bar = foo.activate()
    assert_equal 42, bar
  end

This is called the AAA Pattern because the test case:

  • Assembles a foo object

  • Activates a method on that foo object

  • Asserts the method returned the correct value

All test cases are variations on that pattern. Ideally, a situation should be so transparent that we can write the entire test case before upgrading the production code to obey it.

Why Test-First?

As the great process consultant Jello Biafra once said, "The conveniences we have demanded are now mandatory!" The best way to learn test-first is to write a greenfield project, in which you invent a new module from scratch. You get dozens of test cases, each one to three lines long, and every new case is easier to write. You always know which assertions will fail for the correct reason and force the code to grow. Small test cases help your code decouple in ways you never realized it could.

Greenfield projects make test-first easy to learn, but most of the source code in the world was written without unit tests, and designed without testing in mind. Programmers often call that legacy code, now that they are obsessed with testing. So the hard part of learning test-first is learning to interface with code that resists tests. You can't always figure out how to write the test first, so the tests have less effect on design quality.

Developer-Centric Testing works very well in those situations. The tests help us discover and document what the legacy modules do. We write a little test to assemble our code working with the legacy modules. Then we run the tests, debug to see what the code does, and patch its output values back into the assertions. Call this test-last, but fear it. It might lead to low-quality test cases that only test for syntax, not semantics.

This Short Cut uses test-last each time we attack a new situation. To attack legacy code, use test-last to learn which attacks work, and put the attacks into reusable test methods. Rails comes with a long list of assertion methods that attack legacy web technologies, and we will add a couple more. Reusing assertions designed to attack legacy code helps test-first it.

Target Semantics, not Syntax

All tests start by checking syntax, and all testers should reach for the goal of checking semantics. As a program grows, its tests should defend old features from new changes. A change that improves semantics should not break old tests that check syntax. (See the assert_routing section later in the Short Cut, where we test the semantics of a pretty Uniform Resource Identifier, or URI.) Here's an example of this concept. Suppose the modest method get_html() returns some HTML—a legacy module if ever there was one! We might test it like this:

assert_dom_equal '<b>Betty Boop</b>', get_html()

This assertion does little more than mirror the production code. That code might say, for example, x.b(get_waif_celeb). Both the test and code have a b. (The x is an instance of Builder::XmlMarkup—more on that soon.) Suppose we change the x.b to x.strong, changing the output <b> to <strong>. This change would upgrade the user's experience (strong is more accessible), and it would break the tests. We need tests that help, not hinder, our upgrades.

These "hyperactive" tests should not break too often. Test-first works best when the tests work with the meaning of their assertions. If bold text were mission-critical, you could render your HTML in a browser, then use low-level methods to query the font weight. That would test the feature using processed variables, not raw variables. And it would make the test cases very slow to write and to run.

When developers write tests and code at the same time, editing and testing in tiny cycles, the tests and code have a conversation with each other. If tests don't come first, then the code is in charge of the situation. The tests say, "Could you do this, maybe?" and the code says, "No! I want to do that!" The tests say, "Oh, okay."

When you write tests first, and only write code to pass those tests, the tests learn to command that code. The test case assertions say, "Do this! Do this! Do this!" and the production code says, "Yes sir! Yes sir! Yes sir!" This effect gives us confidence that new tests will rapidly tell codes to do the right thing.

Feedback

When you develop a GUI, you typically use a visual and responsive environment with lots of feedback. Rails, for example, dynamically reloads source files into its web server as you change them. You can save your code, refresh your browser, and see a change instantly. This simple trick turns a lowly web browser into a development environment as dynamic as Visual Studio's Edit and Continue mode. It allows you to change source code while debugging it, so you don't need to restart a program just to see the new code execute. Rails lets you make small edits and see instant results, without "bouncing" (restarting) your server.

An application's logical code, by contrast, runs deep inside its modules, and you can't so easily see what's going on. This explains why Test-First Programming is so popular for that logical code. Your tests become a kind of user interface (the kind that only a programmer could love!). You can make small changes, run the tests, and see instant results, without running your application's slow user interface (which only a user could love).

Test-Driven Development works best when you configure your editor to run all your tests from one unshifted button. You just type, hit the button, and type some more. The editor should remain activated during the test run. See http://phlip.eblogs.com/growl-driven-development/ for some tips on automating a Rails project to run all its tests each time you save a file.

A Wiki Test Runner

To get all these concepts down, we will now create a new Wiki called YarWiki. Its acronym is not for "Yet Another Rails Wiki." YarWiki means "YAML Ajax Rails Wiki" because it uses YAML for the markup language, Ajax for editing, and of course it lives in Rails. Wikis, by definition, are expressly easy to edit, so this new Wiki will make Ajax test cases easy to edit and run.

We need a Wiki that displays, edits, and runs test suites in this format:

  • setup

    • script: login_as :rj

  • test_uncle_wiggily

    • page: /character/uncle_wiggily

    • script: something tests uncle wiggily

  • test_hammy_squirrel

    • page: /character/hammy_squirrel

    • script: something tests hammy squirrel

  • teardown

    • script: rollback

Our Wiki will edit those nodes, and evaluate them to test web pages. We'll focus on formatting those nodes, editing them, and achieving the hard part: evaluating JavaScript assertions directly into a live web page.

The test suite pretends to test a Rails site with a Character controller. We will write no such Rails site; this Short Cut builds a test runner to test that test suite, make it fail, and assert that it presents the failure to the user.

Conspicuously, this project has no internal error handling. A test runner needs perfect error handling, to help everyone write test cases, and to endure any kind of production code. All that coding, however, is straightforward. Simply imagine that for each test case in this Short Cut, we write another test case that does the same thing but introduces an error, then detects our system handled the error correctly.

All that error handling would fill the code up with rescue statements, and would slow down this narrative. So, to get to the point faster, we will be developing in Pleasantville, a wonderful place where nothing ever, ever goes wrong. Except Ajax.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.226.28.197