Small and Simple Web Applications - the Friki Way (Part 6)

Frank Carver, December 2003

Abstract

This article is the sixth of a series which laments the bloated and unmaintainable state of so many J2EE web applications and looks at ways to keep web applications small, simple and flexible. The series uses the author's Friki software as a case study, and discusses ways to design and build a powerful, flexible and usable web application in less space than a typical gif image.

This article carries on working through the user requirements, improving the web application with several pages and the navigation between them.

Introduction

If you've read the first, second, third, fourth and fifth articles, you should be aware that the aim of this project is to develop a small, simple and understandable "Wiki" (editable web site) application. We've considered and decided to defer the relatively heavy decision of how to store and retrieve pages by introducing a Java interface, decided on and implemented a simple "template" system to display pages, and are building a supporting test suite and an automated build script as we go along. Last session we built, deployed and tested a working web application, although it didn't actually do much.

First, let's recap what our "customer" wants, and what we have got so far:

  1. each page may be viewed using it's own unique URL DONE
  2. page content may contain links to other pages by name DONE
  3. links to nonexistent pages will be marked with a "?" DONE
  4. page content may be created and edited using just a browser
  5. at least 100 different pages can be stored

What do we write next?

It should be pretty obvious that the next most important feature is the ability to edit pages. Without this, it's unlikely that anyone would care how many pages could theoretically be held in the system. However, after thinking about how far we got last time, we have another surprisingly tough choice to make. Last session we made a kind of "absolute minimum" solution to the task - all we show is page names, not the actual content. It's pretty obvious that real users are going to want more. Way back in part one we built a template mechanism for just this purpose, and by now I'm certainly itching to use it for real. Doesn't it make more sense to get the page viewing working "properly" before we move on to the next task?

This is a tough choice because there are sensible arguments for both approaches. Moving forward leaving unfinished work behind you can easily create stress and general unhappiness with the code, reducing the effectiveness and speed of further development. The "Pragmatic Programmers" suggest that it's a good idea to "fix broken windows" (see Hunt & Thomas, 2000). On the other hand, time is precious. It's rarely a good investment to spend time on something which may only be changed or discarded later. The Extreme Programming crowd chant "YAGNI: You Aint Gonna Need It!" (see Beck, 2000). In your own projects, you will likely come up against this issue more often than you think. The important thing is to be aware of it and make a conscious, informed, choice rather than just following assumptions.

In this case I'm going to decide that making progress on the next goal is more important, because I reckon being able to create page content in a browser will help us understand more about the page layout and look-and-feel needed to get viewing working right. Of course, this is only a guess, so I may be wrong, but even if this is not the perfect choice in hindsight, it's still better than doing nothing while we try and work out which way to go.

Fourth task: page content may be created and edited using just a browser

The first thing to understand about editing anything in a browser is how the information moves back and forth between the server (where our application is running) and the client (the browser). Let's imagine a user wants to edit the page "FrikiRocks". We'll assume for the moment that this page already exists, and contains some text.

Anyway, enough theorising. let's get down to work. Remember that we always start by adding a new test to our growing test suite. In this case we are testing information flows between client and server, so that sounds like it belongs in "RemoteTests" to me:

RemoteTests.java

package tests;

import junit.framework.*;

public class RemoteTests extends TestCase
{
    public static TestSuite suite()
    {
        TestSuite ret = new TestSuite();

        ret.addTest(new TestSuite(EditPageTest.class));
        ret.addTest(new TestSuite(WebPageTest.class));

        return ret;
    }
}

Note that I have added the new test above the one we wrote last time, so it will be run first. This is a handy trick during development. Remote tests can be relatively slow, and we want to know as soon as possible if our new test is passing or failing. We still need to run those other tests to make sure our code hasn't broken anything, but the important thing is to keep the cycle of testing, learning and improving the code as short as possible.

Obviously, there is no EditPageTest yet, so we'd better write one:

EditPageTest.java

package tests;

import junit.framework.*;
import com.meterware.httpunit.*;

public class EditPageTest extends TestCase
{
    WebConversation http;
    WebResponse response;

    public void setUp()
    {
        http = new WebConversation();
    }

    public void testFetchPageForEdit()
        throws Exception
    {
        response = http.getResponse(
            new GetMethodWebRequest("http://localhost:8080/frikidemo/edit?page=FrikiRocks"));

        assertEquals("example page should return a code of 200 (success)",
            200, response.getResponseCode());
    }
}

Run it, and see what we get:

Testcase: testFetchPageForEdit took 1.016 sec
    Caused an ERROR
Error on HTTP request: 404 Not Found [http://localhost:8080/frikidemo/edit?page=FrikiRocks]

That's OK, and just what we expected, so let's make this little test work by adding an "edit" operation to our application, so we can at least return something. Note that our test doesn't say anything about what information should be returned yet, so the easiest thing is just to tweak the existing web.xml file to add another mapping for "edit". Note in particular that I haven't introduced a new servlet, just adding more mappings to the existing ShowServlet. This is important, because it enables us to test that the config file changes are correct, without getting distracted by having to write and test a separate servlet. We know that the ShowServlet "works", so we use that to make sure our new configurations and test code are correct. Neat.

web.xml

<?xml version="1.0" encoding="ISO-8859-1"?>

<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>
  <servlet>
    <servlet-name>show</servlet-name>
    <servlet-class>friki.ShowServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>show</servlet-name>
    <url-pattern>/show</url-pattern>
  </servlet-mapping>

  <servlet>
    <servlet-name>edit</servlet-name>
    <servlet-class>friki.ShowServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>edit</servlet-name>
    <url-pattern>/edit</url-pattern>
  </servlet-mapping>

</web-app>

Well, it passes this first test, but of course it's a long way from editing a page yet. I think the next test should be one to prompt us to write that new servlet. But, before we go on, have you noticed a similarity between the code in "WebPageTest.java" from last session, and "EditPageTest" from this session? I have, and I want to remove the duplication before we move on. First, let's copy the common code out to a new class:

RemoteWebTest.java

package tests;

import junit.framework.*;
import com.meterware.httpunit.*;

public class RemoteWebTest extends TestCase
{
    WebConversation http;
    WebResponse response;

    public void setUp()
    {
        http = new WebConversation();
    }

    public void fetchPage(String url)
        throws Exception
    {
        response = http.getResponse(new GetMethodWebRequest(url));
        assertResponseCode("URL '" + url + "' should return 200 (success)", 200);
    }

    public void assertResponseCode(String message, int code)
        throws Exception
    {
        assertEquals(message, code, response.getResponseCode());
    }
}

Note that we have put an assertion that the page returns a success code in the "fetch" method. This may or may not be a good idea in the long run, but it certainly helps simplify our code right now. Now we can trim our existing test cases:

EditPageTest.java

package tests;

import junit.framework.*;

public class EditPageTest extends RemoteWebTest
{
    public void testFetchPageForEdit()
        throws Exception
    {
        fetchPage("http://localhost:8080/frikidemo/edit?page=FrikiRocks");
    }
}

WebPageTest.java

package tests;

import junit.framework.*;

public class WebPageTest extends RemoteWebTest
{
    public void testApplicationPresent()
        throws Exception
    {
        fetchPage("http://localhost:8080/frikidemo");
    }

    public void testServerPresent()
        throws Exception
    {
        fetchPage("http://localhost:8080/");
    }

    public void testExamplePage()
        throws Exception
    {
        fetchPage("http://localhost:8080/frikidemo/show?page=ExamplePage");
    }
}

That's a lot neater. It's easier to read and understand (it clearly shows that our tests don't actually test much, for example.) We've isolated the dependencies on the HTTPUnit test framework into a single class, and things are generally smaller. Now, back to adding that next test.

The idea of the next test is pretty simple. To start with we just want to check that when we ask to edit the page "FrikiRocks", we get back the content in an editable box. However, for an HTML page to make sense, it probably needs to contain a load more than that: body tags, head tags, product logos, descriptive text, positioning, copyright messages and so on.

We could build our tests by hard coding the whole HTML page in the test, and writing asserts that ensure that every single character is sent correctly. This is simple to write at the start of a project, and can seem a good idea. But beware. Any change to the "look and feel" of the application means that the HTML for many pages will change. Any change to the HTML for lots of pages is likely to break lots of tests, even though what each test is supposed to be testing probably still works. Tests that can fail when unrelated code, data or configurations change are known as "brittle". Brittle tests are one of the main reasons why automated testing is considered hard to do, expensive to maintain, and easy to get wrong..

So, let's plan ahead, and build our application with testing in mind. In this case we are testing that (regardless of whatever else there is on the page) it contains a form with a textarea containing the text we want. If we define that the form has a specific "id" attribute, we can easily ignore the rest of the page, and concentrate our attention:

EditPageTest.java

package tests;

import junit.framework.*;
import com.meterware.httpunit.*;

public class EditPageTest extends RemoteWebTest
{
    public void testFetchPageForEdit()
        throws Exception
    {
        fetchPage("http://localhost:8080/frikidemo/edit?page=FrikiRocks");

        WebForm form = response.getFormWithID("editform");
        assertEquals("textarea 'content'", "For Sure", form.getParameterValue("content"));
    }
}

Note that some HTTPUnit code has crept back in (for the moment at least.) This may well end up being pushed "up" in to the RemoteWebTest parent class, but there's no real need or reason to do that yet. We can leave that decision until we have more than one place where the code is used. Meanwhile, this test fails, of course. We don't have any code in our real application to produce a form or textarea yet.

First, let's change our web.xml deployment descriptor to use a real EditServlet class rather than reusing ShowServlet:

web.xml

  ...
  <servlet>
    <servlet-name>edit</servlet-name>
    <servlet-class>friki.EditServlet</servlet-class>
  </servlet>
  ...

Now, we take the quickest route to working code. Copy the old ViewServlet code to a new file, and modify it to generate an "edit" page instead of a "show" page:

Just one thing to mention, Before all those more knowledgeable or thoughtful readers start to complain. You will notice below that I "hard code" HTML into the code of a servlet. This is not a good idea. It's hard to maintain, easy to screw up, and bloats the code with unneeded data. However, at this point in developing this application it is a reasonable thing to do. Remember that our plan is to eventually move all HTML out to external templates, but doing that now would un-necessarily complicate things. We are not testing (or writing) the HTML used by the final application, we are testing the information flow between pages during editing, and this is merely a simple and robust way to do it.

EditServlet.java

package friki;

import java.io.IOException;
import java.io.Writer;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class EditServlet
    extends HttpServlet
{
    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        String name = req.getParameter("page");
        Writer writer = res.getWriter();
        writer.write(
            "<html><head><title>Page " + name + "</title></head>" +
            "<body>\n" +
            "<h2>Edit Page '" + name + "'</h2>\n" +
            "<form id='editform' method='POST' action='update'>\n" +
            " <input type='hidden' name='page' value='" + name + "'>\n" +
            " <textarea name='content'>For Sure</textarea>\n" +
            "</form>\n" +
            "</body></html>"
            );
        writer.flush();
    }
}

Run it, and it all works. Excellent

Note that we're still hard coding the page content at the moment as well, because that's all our current tests need. In effect, our system has only one page. Sure, we'll need to fetch pages from our page store at some point, but until we have tests for that, there's no point worrying about the code.

Now, in order to change this content and send it back, we'll need some sort of "send" button. So let's add this to our test. Just as before, we'll look for a specific "id" for the button, rather than a particular name or value, so that page designers or user-interface specialists can tune-up the interface without breaking simple functionality tests like this one.

EditPageTest.java

    ...
    public void testFetchPageForEdit()
        throws Exception
    {
        fetchPage("http://localhost:8080/frikidemo/edit?page=FrikiRocks");

        WebForm form = response.getFormWithID("editform");
        assertEquals("textarea 'content'", "For Sure", form.getParameterValue("content"));

        SubmitButton sendButton = (SubmitButton)form.getButtonWithID("sendbutton");
        assertTrue("form needs a 'send' button", sendButton != null);
    }
    ...

EditServlet.java

        ...
        writer.write(
            "<html><head><title>Page " + name + "</title></head>" +
            "<body>\n" +
            "<h2>Edit Page '" + name + "'</h2>\n" +
            "<form id='editform' method='POST' action='update'>\n" +
            " <input type='hidden' name='page' value='" + name + "'>\n" +
            " <textarea name='content'>For Sure</textarea>\n" +
            " <input type='submit' id='sendbutton' name='SEND' />\n" +
            "</form>\n" +
            "</body></html>"
            );
        writer.flush();
        ...

Good. Now we have a button, we can use it to submit the form, and see what happens. Remember that full details of all the HTTPUnit APIs I use here can be found at http://httpunit.sourceforge.net/doc/api/index.html

EditPageTest.java

        ...

        SubmitButton send = (SubmitButton)form.getButtonWithID("sendbutton");
        assertTrue("form needs a 'send' button", send != null);

        WebRequest sendback = form.getRequest(sendButton);
        sendback.setParameter("content", "For Sure.\nThanks, Dude!");
        response = http.getResponse(sendback);
        assertResponseCode("upload should return a code of 200 (success)", 200);
        ...

Can you guess what happened? Error on HTTP request: 404 Not Found [http://localhost:8080/frikidemo/update]. Looks like HTTPUnit is happy with what we are asking it to do - it's examined the form and its button, and worked out to try and send the content to "update". Unfortunately, we have no update servlet to receive the new page content. Let's quickly make one. And don't forget to add it to the web.xml mappings.

UpdateServlet.java

package friki;

import java.io.IOException;
import java.io.Writer;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class UpdateServlet
    extends HttpServlet
{
    public void doPost(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        String name = req.getParameter("page");
        Writer writer = res.getWriter();
        writer.write(
            "<html><head><title>System Message</title></head>" +
            "<body>Page '" + name + "' updated successfully</body></html>");
        writer.flush();
    }
}

web.xml

  ...
  <servlet>
    <servlet-name>update</servlet-name>
    <servlet-class>friki.UpdateServlet</servlet-class>
  </servlet>

  <servlet-mapping>
    <servlet-name>update</servlet-name>
    <url-pattern>/update</url-pattern>
  </servlet-mapping>
  ...

Now it works again. Good.

One thing is beginning to bug me, though. We now have three servlets containing very similar code. I'm sure we can factor out that duplication before we move on. Let's start (as usual) by copying out the common bits to a new class:

FrikiServlet.java

package friki;

import java.io.IOException;
import java.io.Writer;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public abstract class FrikiServlet
    extends HttpServlet
{
    protected void doBoth(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        String name = req.getParameter("page");
        Writer writer = res.getWriter();
        process(name, writer);
        writer.flush();
    }

    public void doPost(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        doBoth(req, res);
    }

    public void doGet(HttpServletRequest req, HttpServletResponse res)
        throws ServletException, IOException
    {
        doBoth(req, res);
    }

    protected abstract void process(String name, Writer writer)
        throws IOException;
}

Note that two of our existing servlets use "doPost", and one uses "doGet". I've chosen to use one of the common idioms for this, and implemented both doGet and doPost in the base class, but called a single method from both. This is handy, because it allows us to freely choose whether to use HTTP GET or POST requests anywhere in our application. Note also that I have made this class "abstract", and added an abstract method to do the actual processing (the bit where the three servlets differ.) Now we can strip out all that duplicated code in the three servlets:

ShowServlet.java

package friki;

import java.io.IOException;
import java.io.Writer;

public class ShowServlet
    extends FrikiServlet
{
    protected void process(String name, Writer writer)
        throws IOException
    {
        writer.write(
            "<html><head><title>Page " + name + "</title></head>" +
            "<body>This is Page '" + name + "'</body></html>");
    }
}

EditServlet.java

package friki;

import java.io.IOException;
import java.io.Writer;

public class EditServlet
    extends FrikiServlet
{
    protected void process(String name, Writer writer)
        throws IOException
    {
        writer.write(
            "<html><head><title>Page " + name + "</title></head>" +
            "<body>\n" +
            "<h2>Edit Page '" + name + "'</h2>\n" +
            "<form id='editform' method='POST' action='update'>\n" +
            " <input type='hidden' name='page' value='" + name + "'>\n" +
            " <textarea name='content'>For Sure</textarea>\n" +
            " <input type='submit' id='sendbutton' name='SEND' />\n" +
            "</form>\n" +
            "</body></html>"
            );
    }
}

UpdateServlet.java

package friki;

import java.io.IOException;
import java.io.Writer;

public class UpdateServlet
    extends FrikiServlet
{
    protected void process(String name, Writer writer)
        throws IOException
    {
        writer.write(
            "<html><head><title>System Message</title></head>" +
            "<body>Page '" + name + "' updated successfully</body></html>");
    }
}

Don't forget to re-run all the tests after these changes. The tests we have been building as we go along form the essential "safety net" to allow us to do sweeping simplifications like this without fear.

I think this is a reasonable place to stop this session. I think we've done enough to be confident that we have the process for editing pages in place. As usual, I encourage you to think about other tests you could add to the test suite, and make your own decisions on whether they would be useful, or whether they might make the test suite more "brittle". And please remind any web UI designers you happen to meet about the huge testability benefits of using "id" attributes in their web designs.

How are We Doing?

We still haven't made a Wiki yet! But we have just about completed another of our user goals, and have built, deployed and run a growing web application. Best of all, we didn't abandon our commitment to testing everything (even things that seemed easy to write and hard to test). We can say with confidence that whatever we do next, it won't sneakily break what we have written so far. And our application still fits in a 7K war file.

Next session we will attack the last of these customer goals, which requires that we finally tie together the web user interface with the page storage. With any luck, we'll be able to use that page template code, too!. Hmm? You said I promised a usable web application this time? Point your browser at http://localhost:8080/frikidemo/edit?page=FrikiRocks and have a play. It doesn't store changes (we haven't done that goal yet), but you can enter text, click buttons and watch it do its stuff. It may look rough, but it can be used . . . Sort of.

References