Magazine PDF Issue Conference Forum Software & Support Verlag











Coding a Login Box Shouldn't Hurt

How Automated Web Tests Can Get Your Development Up To Speed
by Marcus Baker

A login box is a couple of text fields and a button. A ten-minute job, right? Well, coding the first version probably is, but does it work? Easy - fire up the browser, enter a username and password and click Submit - Success!! Really? Did it set the cookie or was it set from my last test? OK, try again. Clear my cookies, log in again, and ... it fails! Oh! I'm already logged in - OK, clear the cookies, clear the database session, log in, and check the cookies. I had better test a wrong password as well - clear the cookies, clear the database session, log in, check the cookies...There has to be a better way!

One big problem with Web development, and writing user authentication code in particular, is the complexity of the environment. It is necessary to juggle not just the HTML forms interface, but also cookies, database data, and time-outs. Authentication code is also mission critical. Customer login failures can create baffling and expensive support requests that often trickle down to the technical staff. Not to mention protected data becoming compromised by hackers and robots. The result is not just an environment that is difficult for testing, but also one that also requires a great deal of it.

A Simple Authenticator
We won't get anywhere if we don't at least try, so by illustration we are going to code the simplest possible login box. Here, I assume that we have discussed the needs of the Web site with an imaginary client and have captured their needs as a user story. Let's start with some requirements - following is a user story (see the User Stories panel) for a very simple login system:
  • A valid username and a password is required. If a user gets this wrong then he/she gets a helpful message on the same page. They do not have to re-enter the username, but the password is cleared.
  • After a successful login the browser remembers the username, but not the password. This memory lasts ninety days and is intended simply as a memory jogger to reduce support requests; it has no effect on authentication.
  • The user may only log in with one browser at a time. The time-out for a dead session has yet to be decided.
These requirements are fairly typical - it ignores authorisation (permissions) and user registration, as these tend to be separate problems. Instead, it concentrates on password authentication. The storage solution is not yet defined either.

User Stories
User stories originated in the eXtreme Programming methodology and are described at http://www.extremeprogramming.org/rules/userstories.html. A user story tends to be light on technical detail, has business value and is written mostly by the client. The developer can assist with this, which is often necessary when technical issues such as browsers are involved. With our stories I have had to clarify with the client how a user is restricted to a single session at a time, but that is all. The resulting story contains little technical detail except where the developer has asked some probing questions, but it does express the business and interaction rules as emphatically as possible. They are a kind of lightweight use case, the idea being to get moving faster so that we can get working code in front of our client.

Use cases tend to be more formal and have a fixed format. To make up for the lack of detail, stories are usually augmented with acceptance tests, preferably dictated by the client. If client contact is limited you should consider the more detailed and more analytical use case system. With use cases much more attention is placed on covering failure modes and analysing all variations. With stories we are hoping to fix things up as we go. Both use cases and user stories give a great starting point for designing the test regime.


By way of a prototype we can start with a simple login.php page -- it won't win any praise from a graphic designer (see Fig. 1) -- for now we'll just concentrate on the logic and keep everything else as simple as possible:

Listing 1

<html>
<head><title>Log-in</title></head>
<body>
<form>
<h2>Please log-in</h2>
Username:
<input type="text"
name="u"/><br />
Password:
<input type="password"
name="p"/><br />
<input type="submit"
name="e"
value="Enter"/>
</form>
</body>
</html>


Fig. 1: Simplest Possible Login Form

We could start writing code now, but I am not so eager to enter the lengthy debug and inspection cycles just yet. Instead, let's take a step back and see if we can take a little more control of the development process.Let the Machine Do the DrudgeThe time consuming aspect of writing user interface code is the visual inspection of the results. If we could avoid the Web browser interaction, the test procedure could be automated to reduce all of those mouse clicks to just one. This isn't too difficult to do. Since we are dealing with a text protocol, namely HTTP, we can simulate all the browser actions by sending appropriate text to UNIX(tm) sockets. There are quite a few libraries available that communicate with HTTP and parse the resulting headers and HTML, so it is possible to script this ourselves. It's not trivial though, so it would be best to look for a tool to help us.

Not surprisingly, there are plenty of tools available (see http://www.softwareqatest.com/qatweb1.html - FUNC for a large list). Unfortunately most of these tools either use Java or Perl, or a less flexible format such as XML or plain text. Because we are going to be doing some fairly detailed testing we need a full programming language. Also, we don't want to rewrite any of our other supporting code, so it is best to use PHP. At the time of writing this restricts our choice to SimpleTest (http://simpletest.sourceforge.net/) -- along with Harry Fuecks and Jason Sweat, I am one of the authors of this test framework -- it is closest in intent to the Java tools Junit and JwebUnit, and is organised around test cases in the same way.

For a developer the worst case scenario is debugging. Now we cannot avoid bugs, but the quicker we find a bug, the easier it is to fix. Since tests catch bugs we should run the tests first. For maximum benefit we will run them even before we write the code. Here is the starting test script (test.php):

Listing 2

<?php
define('SIMPLE_TEST',
'/var/www/simpletest/');
require_once(SIMPLE_TEST . 'web_tester.php');
require_once(SIMPLE_TEST . 'reporter.php');

$test = &new GroupTest('All tests');
$test->addTestFile ('login_test.php');
$test->run(new HtmlReporter());
?>

Installing SimpleTest is as simple as unpacking the tarball from SourceForge. The first line of our test script sets a constant to point at the unpacked download. For this example, I have unpacked it to /var/www/. SimpleTest includes a suite of testing tools - for now, we just need the Web tester and a means of displaying our results, so we include only these two. The line:

$test = &new GroupTest('All tests');

creates a new group test. We could create our tests for this article as a single script, but in real applications, with several thousand tests, we need a way to scale things. For this reason we have a top-level script that will load test cases from other files. We haven't actually written our test case file yet, but once we have, it will execute with GroupTest::run(), which takes a type of display object as a visitor (another pattern). SimpleTest includes some very crude display classes of which HtmlReporter is one, but the idea is that you would evolve your own within a project -- more on this later.

To get us up and running, here is a 'do nothing' login_test.php file:

Listing 3

<?php
class LoginTestCase extends WebTestCase {
function LoginTestCase() {
$this->WebTestCase();
}
}
?>

If you have been typing this code as you go, you can now invoke the test.php script in your Web browser of choice, to see an output similar to that shown in Fig. 2.


Fig. 2: Test Run with an Empty Test Case

Next, let's write a test. An obvious and easy test is that an unknown username and password will fail to get entry - we can express this by adding a test to our test case:

Listing 4

class LoginTestCase extends WebTestCase {
function LoginTestCase() {
$this->WebTestCase();
}
function testUnknownUserGetsAnError() {
$this->get ('http://uno/login/login.php');
$this->setField('u', 'Anybody');
$this->setField('p', 'Hacker');
$this->clickSubmit('Enter');
$this->assertWantedPattern('/unknown username/i');
}
}

This process is the core of the testing system. When a test case is run it uses reflection to execute any methods, within itself, that have names starting with test. Other methods are ignored and can be used freely, but these methods are special. Each test method is invoked in order and it communicates with the test reporter by means of assertion methods. This system is identical to most unit testers, such as Junit and PHPUnit, and SimpleTest as well.

Next, we'll look at the Web page interactions. The WebTestCase object creates an internal Web browser at the start of each test method. Our first instruction to it is to send a GET request to our minimal login page. I have hard-coded the URL here, which is slightly bad practice since the tests won't be portable. For production purposes, it would be necessary to use a configuration file that is crafted to your personal taste.

Our next steps are to fill out the fields and submit the form. SimpleTest tries to act like a typical Web browser and, in finding no method or action attribute on the form tag, will resubmit to the current URL. The WebTestCase::assertWantedPattern() method scans the resulting page looking for a regular expression match. If it finds one, it sends a pass event to the test reporter, else we'll get a fail event. If we refresh the test script in our Web browser we get a failure, like that shown in Fig. 3.


Fig. 3: No Code Yet ...

Now it's time to write some code.

Test Driven Development
This style of development, working in small cycles and first writing a test each time, is known as test driven development. The rule is that we don't write new functionality unless we have a failing test -- once we do have the failed test, the next step is to get that test to pass as quickly as possible. Here is a login.php script with the 'passed' test:

Listing 5

<html>
<head><title>Log-in</title></head>
<body>
<form>
<h2>Please log-in</h2>
Username:
<input type="text" name="u"/><br />
This is an unknown username.<br />
Password:
<input type="password" name="p" /><br />
<input type="submit" name="e" value="Enter" />
</form>
</body>
</html>

Here, I am trying to be antagonistic to my own test case in the hope of finding loopholes. It is pretty obvious that the above script will always display the error message even if the user hasn't entered anything yet. Blatantly incorrect, but ponder the complexity to come -- it is quite possible that this event may occur by accident and our tests don't catch this yet.

Let's modify our test, this time making sure that the page starts without the message -- this warrants just one extra line of test code:

Listing 6

class LoginTestCase extends WebTestCase {
...
function testUnknownUserGetsAnError() {
$this->get}('http://uno/login/login.php');
$this->assertNoUnwantedPattern('/unknown username/i');
$this->setField('u', 'Anybody');
$this->setField('p', 'Hacker');
$this->clickSubmit('Enter');
$this->assertWantedPattern('/unknown username/i');
}
}

With this additional constraint we are finally forced to write some real code:

Listing 7

...
<form>
<h2>Please log-in</h2>
Username:
<input type="text" name="u" /> <br />
<?php
if (isset($_GET['e'])) {
print 'Unknown username. <br />';
}
?>
Password:
<input type="password" name="p"/><br />
<input type="submit" name="e" value="Enter" />
</form>
...

The code looks a bit hacky right now, however, it's quite normal at this stage because we don't want to over-design -- something that is easily done with only a few requirements. You could add a templating system or application framework into the mix if your architecture demands it -- the style of development would be the same. Do the simplest thing and rely on the tests to prevent you from making it too simple. Writing code beyond the requirements is wasteful, not just for us, but also for every other developer who will have to read our code. For this article we'll stick to using PHP since we need to get our first version up and running quickly.

Now, test first is actually a fast process. Since we never go further than a very small step we keep a steady consistent pace and we keep it bug free. Not counting writing this text, it has only taken me a couple of minutes to get this far. The other good news is that our test now passes (see Fig. 4).


Fig. 4: Progress At Last

Top-Down Design
If you are an object bigot you wouldn't want to talk directly to the database -- it would be preferable to talk to a few top-level objects in the domain we are working in, and know nothing else about the machinery underneath. Right now we just need a domain object that will successfully authenticate a legitimate test user so that we can pass the next test:

Listing 8

class LoginTestCase extends WebTestCase {
function LoginTestCase() {
$this->WebTestCase();
}
function testUnknownUserGetsAnError() {
...
}
function testKnownUserCanLogIn() {
$this->get ('http://uno/login/login.php');
$this->setField('u', 'Person');
$this->setField('p', 'Secret');
$this->clickSubmit('Enter');
$this->assertWantedPattern('/logged in/i');
}
}

For the moment I'll pretend that this ideal object already exists and code the script accordingly. The following script shows a new version that does script-level authentication -- it is still rather hacky, but let's just throw the pieces into play and rework it later:

Listing 9

<?php
require_once('authenticator.php');

if (isset($_GET['e'])) {
$authenticator = &new Authenticator();
$is_valid = authenticator->isValid(
$_GET['u'], $_GET['p']);
if ($is_valid) {
header('Location:' .'http://uno/login/welcome.php');
exit();
}
}
?><html>
<head><title>Log-in</title></head>
<body>
<form>
<h2>Please log-in</h2>
Username:
<input type="text" name="u" /><br />
<?php
if (isset($_GET['e'])) {
print 'Unknown username.<br />';
}
?>
Password:
<input type="password" name="p" /><br />
<input type="submit" name="e" value="Enter" />
</form>
</body>
</html>

To get all of this to work we'll need a landing page for the redirect. We don't have to worry what happens after that, as that is the subject of authorisation. This is a very different task than authentication and so any code we write could prejudice a solution on another part of the project. It is all too simple to start adding session support here, but this is scope creep. It is bad enough when customers do it without developers inflicting it upon themselves. This is another example of where simplicity is the best approach. We don't have any requirements for this new page yet, so we'll just fake it:

<html>
<body>
Logged in.
</body>
</html>

The Authenticator class is more problematic and depends on our development strategy. If we want to complete the user story we should start work on this class straightaway, letting our login script drive the initial requirements. If we were in the middle of the project that would be our usual approach. On the other hand, if we were still designing the overall site it would be silly to get bogged down in implementation details until the site structure settles down. In such situations, a top-down design is preferable. So our next step is to write a temporary stub class for authenticator.php:

Listing 10

<?php
class Authenticator {
function isValid($username, $password) {
if (trim($username) != 'Person') {
return false;
}
if (trim($password) != 'Secret') {
return false;
}
return true;
}
}
?>

If an Authenticator had already been written, it would certainly clash with our tests. If this happens then we have two choices -- either use the real authenticator and add some test code to enter a user in the database, or generate a fake wrapper. Whichever way we go we need a tightly controlled test. Tests that are unpredictable are almost useless since they don't increase confidence in the code. I find that writing test cases is very easy and linear. It is dictating the exact environment that the test runs in that requires some creativity.

With these components in place the tests should now pass. Not only that, but a design has started to emerge as well, a design that starts life both minimal and tested.

Despite this you may feel we haven't gained much yet over conventional programming so let's increase speed and tackle some of the tricky parts of the application. First, let's tie up a loose end -- if the user mistypes their username, they should not have to retype it again, but the password has to be retyped always. Here is the test method:

Listing 11

class LoginTestCase extends WebTestCase {
...
function testFailedLoginKeeps UsernameOnly() {
$this->get ('http://uno/login/login.php');
$this->assertField('u', '');
$this->assertField('p', '');
$this->setField('u', 'Anybody');
$this->setField('p', 'Hacker');
$this->clickSubmit('Enter');
$this->assertField('u', 'Anybody');
$this->assertField('p', '');
}
}

The WebTestCase::assertField() method tests the current value of a form field. Here, we first check if the u field is empty and after a rejection it is pre-filled with the last attempt -- not too difficult to read once you have seen a few test cases. The following code bit preserves the field in the short term, however, for the next user story we need cookies:

Listing 12

...
<form>
<h2>Please log-in</h2>
Username:
<?php
print '<input type="text" name="u"';
if (isset($_GET['e'])) {
print ' value="' . $_GET['u'] . '"';
}
print " /><br />\n";
if (isset($_GET['e'])) {
print 'Unknown username.<br />';
}
?>
Password:
<input type="password" name="p" /><br />
<input type="submit" name="e" value="Enter" />
</form>
...

Cookie Tests
The WebTestCase class' test methods always start with an empty browser. If we want to start with some history we must set that history up by making a sequence of page requests. Following is the test case for the second part of our user story:

Listing 13

class LoginTestCase extends WebTestCase {
...
function testRemembersUsernameAfterLogin() {
$this->get ('http://uno/login/login.php');
$this->setField('u', 'Person');
$this->setField('p', 'Secret');
$this->clickSubmit('Enter');

$this->restartSession();
$this->get ('http://uno/login/login.php');
$this->assertField('u', 'Person');
$this->assertField('p', '');
}
}

We log in as before, and then to simulate the follow on browser shutdown and restart we invoke WebTestCase::restartSession(). This will clear any temporary cookies as well as any that have timed out through age. SimpleTest allows us to test and manipulate cookies directly with methods like WebTestCase::setCookie(). While this is convenient, it is usually best avoided since it can make the tests too dependent on details such as the cookie name.

After the simulated browser shutdown we go back to the login.php page and test that the username is prefilled. Besides adding the extra lines of code to the login script I also decided to tidy things up -- during this shuffling exercise (called refactoring) I ran the tests several times to ensure I hadn't broken any previous code. Testing this script thoroughly, without automation, would have turned this five-minute step into a real chore, but now with one click testing there is more time for code quality. The code looks a lot cleaner with a format that allows wider text lines and the main code block as a separate file:

Listing 14

<?php
require_once('authenticator.php');

if (isset($_GET['e'])) {
$authenticator = &new Authenticator();
$is_valid = $authenticator->isValid($_GET['u'], $_GET['p']);
if ($is_valid) {
setcookie('u', $_GET['u'], time() + 90*24*60*60);
header('Location: ' .'http://uno/login/welcome.php');
exit();
}
$u = $_GET['u'];
} elseif (isset($_COOKIE['u'])) {
$u = $_COOKIE['u'];
} else {
$u = '';
}
?><html>
<head><title>Log-in</title></head>
<body>
<form>
<h2>Please log-in</h2>
Username:
<input type="text" name="u" value="<?php print $u; ?>" /><br />
<?php
if (isset($_GET['e'])) {
print 'Unknown username.<br />';
}
?>
Password:
<input type="password" name="p" /><br />
<input type="submit" name="e" value="Enter" />
</form>
</body>
</html>

Because the cookie based prefill has nothing to do with authentication I have kept it out of the Authenticator domain object and left it within the login script itself. The only remaining task is to enforce the time-out on the cookie by adding another test:

Listing 15

class LoginTestCase extends WebTestCase {
...
function testForgetUsernameAfter90Days() {
$this->get ('http://uno/login/login.php');
$this->setField('u', 'Person');
$this->setField('p', 'Secret');
$this->clickSubmit('Enter');

$this->ageCookies(90*24*60*60 + 1);
$this->restartSession();
$this->get ('http://uno/login/login.php');
$this->assertField('u', '');
$this->assertField('p', '');
}
}

The WebTestCase::ageCookies() method saves us having to sit around for ninety days and one second just to see if our test has passed. When we run this test, it passes straightaway without any further code changes. So is this test a waste of time? No, besides preventing breakage in the future (called regression testing), it also helps to document the code. If you look at all of the tests we have written so far what you see is a specification that is readable by developers. Not only that, it is a specification that can be checked by a machine. If you want to find out how a part of the application really works, rather than how the comments say it works, then go straight to the tests -- this is another powerful side-effect of this style of coding.

Multiple Browsers
The final part of our user story is prevention of multiple logins from a single account. This is a complex issue and there is not enough space to go into an implementation, so, we'll just concentrate on the testing side of things.

The WebTestCase class has a single Web browser working behind the scenes, but for multiple browser problems that is too high an abstraction. SimpleTest allows access to the browser class directly -- instead of the WebTestCase we can use the lower-level UnitTestCase, which means that we'll have to modify our original test.php runner script:

Listing 16

<?php
define('SIMPLE_TEST', '/var/www/simpletest/');
require_once(SIMPLE_TEST . 'web_tester.php');
require_once(SIMPLE_TEST . 'unit_tester.php');
require_once(SIMPLE_TEST . 'browser.php');
require_once(SIMPLE_TEST . 'reporter.php');

$test = &new GroupTest('All tests');
$test->addTestFile ('login_test.php');
$test->run(new HtmlReporter());
?>

With the extra libraries included we can just add our new test case class to the existing login_test.php file:

Listing 17

<?php
class LoginTestCase extends WebTestCase {
...
}

class MultipleLoginTests extends UnitTestCase {
function MultipleLoginTests() {
$this->UnitTestCase();
}
function testBlocksSecondLogin() {
$browser1 = &new SimpleBrowser();
$browser1->get('http://uno/login/login.php');
$browser1->setField('u', 'Person');
$browser1->setField('p', 'Secret');
$browser1->clickSubmit('Enter');
$this->assertWantedPattern('/logged in/i', $browser1->getContent());

$browser2 = &new SimpleBrowser();
$browser2->get('http://uno/login/login.php');
$browser2->setField('u', 'Person');
$browser2->setField('p', 'Secret');
$browser2->clickSubmit('Enter');
$this->assertWantedPattern('/already logged in/i',
$browser2->getContent());
}
}
?>

The SimpleBrowser class does not have built-in assertion methods, thus making the syntax a little unclear. For example, we have to extract the raw content from the browser so that we can use UnitTestCase::assertWantedPattern() for the comparison. Besides these syntactic differences, all of the same features are available in the SimpleBrowser class as in the WebTestCase class.

This test should probably be commented out until more work is done on the authenticator. If you leave it failing it will nag you. It will also rob you of the instant feedback of the red/green rhythm. This rhythm is what makes all of the preparation worthwhile. You may feel that the progress is slower, but it actually gets more productive as the problems become more complex.

Acceptance Tests
By now it must have occurred to you that if SimpleTest had a nicer looking report mechanism, and you wrote all the requirements as a set of tests at the start, you could show your client the test results as a measure of project progress. Even better, if they could write the tests it would make the entire project customer driven -- no more disputes over sites being finished or not. Perfect communication of the requirements at last? Client driven or -written tests are usually called acceptance tests and it looks as if completing this customer feedback loop should be possible. Unfortunately, producing a tool for this is actually more work than what it appears to be.

The first difficulty is in reporting. The display is kept brief to ensure that the developer encounters only the minimum cognitive load while coding. You really want to see bugs straightaway in no uncertain terms and scrolling through output will just break your concentration. Clients on the other hand would prefer to see passes rather than failures. Not too much of a challenge though -- writing a more formal SimpleTest Reporter class is relatively easy, and there is an explanation on how to do this within the SimpleTest package.

The problem is the language the tests are written in. It may not be possible to write PHP test code while sitting with a client -- the listening process is hard enough without having to focus on PHP syntax as well. Further, the client certainly cannot help unless they know PHP, and XML wouldn't be any better. In such situations, it might be acceptable to write tests as bulleted lists in the word processor, like so:

Get page http://uno/login/login.php
set username to Person
set password to Secret
click submit button marked Enter
...

A document that includes such lists, mixed in with freeform written specifications, would be client-friendly. This file could be parsed, the tests could be generated and run, and the test reporter could mail out a modified version, marking passes or fails against the tests. Alternatively, you could publish the test results on an extranet so that the client can run the tests and monitor progress. One tool that already does this for Java is FIT -- it uses a wiki for publishing so that the test cases can be edited on the Web as well -- however, it's fiddly to set up and feels experimental. The Java Naked Objects library has a built in acceptance-testing feature, but it commits the cardinal sin of being written in a programmer's language -- Java. Even outside the PHP world this whole area is a new field of research.

At the time of writing there aren't any acceptance test tools specifically for PHP. With the rise of lean and agile methodologies, direct customer value is gaining in importance. If you were thinking of rescuing the development community by writing a better editor or yet another framework it might be time to think again. The productivity gains will be marginal compared with the leap forward that can be made with better communication. And if you should discover something new, perhaps you could write an article.
About the Author: Marcus Baker is a consultant and senior Web developer for Wordtracker. He is a co-founder of the PHP-London organisation, and is active on Sitepoint as well.

Links and Literature
  • Click here to download the source code for the article.
  • TDD in unit testing is clearly described in Test Driven Development by Example, by Kent Beck. The examples are in Java and Python, but they are easy to follow.
  • If you are a manager as well as a developer, Lean Software Development: An Agile Toolkit for Software Development Managers, by Mary and Tom Popendieck is an excellent introduction to the value chain in software development.
  • If you haven't heard of them before, Writing Effective Use Cases, by Alistair Cockburn is a classic book on translating requirements into software. One of the major researchers in the software methodology field.
  • Finally, if the phrases composite pattern and visitor pattern were new to you then I am afraid that this book is your homework assignment: Design Patterns, by Gamma, Helm, Johnson and Vlissides. Not an easy book, but essential for programmers working in this now object oriented world.

Software & Support Verlag - Global Alliance Program!







-- Advertisement --
Kelkoo price comparison in Germany
- Mobiles
- Furniture
- Notebooks
- Hotels
- Flights
- Digital cameras
Software & Support Verlag GmbH