Industrial Logic -> Papers -> A Test-First Design eXPerience

A Test-First Design eXPerience

Version: 1.0
 Author: Joshua Kerievsky
Created: March 12, 2000

Testing Web Pages

I need to write some code to test the contents of Web pages. This is functional test code. eXtreme Programming (XP) says that Customers write functional tests, so this code is going to execute on behalf of Customers. Now I have all sorts of ideas in my head about how to do this, but XP also tells me to start simple, and use a test-first approach to build my software. OK, I'd like you to join me as I build this software.

Step 1. Write my first test. I must admit, this is wierd. It is natural to start writing the actual implmentation code, but to start by writing a test is odd. What do I call the test code class name? Well, since I have some idea that what I'm going to create is going to be called a Page, I'm going to call this test code PageUnitTest.java. And here's what I wrote:

1   import junit.framework.*;
2    
3   public class PageUnitTest extends TestCase {
4    
5     public PageUnitTest(String name) {
6       super(name);
7     }
8    
9     static Test suite() {
10       TestSuite suite= new TestSuite();
11    
12       suite.addTest(
13          new PageUnitTest("testPageCreation") {
14            protected void runTest() {
15              this.testPageCreation();
16            }
17          }
18       );
19    
20       return suite;
21     }
22    
23     public void testPageCreation() {
24       Page page = new Page();
25       assertNotNull(page);
26     }
27   }

I go to compile this code and it doesn't even compile. Says that Page doesn't exist. Well, I knew that. I just had to let the compiler tell me. So I create the Page class as follows:

1   public class Page {
2   }

I compile and then go back to PageUnitTest to compile it. The compile now succeeds. So I execute it as a test. Since I use JUnit, the way I do this is I execute the following:

java junit.textui.TestRunner PageUnitTest
When this runs, it outputs the following:
.
Time: 0.50

OK (1 tests)

Great. Now I feel good. I'm doing test-first design and I've passed a test! Now it is time to do some real work.

I think of a Page as a representation of a web page, which is returned from a Web server via a Servlet, or JSP (Java Server Pages) or CGI program. I have to specify a URL for this page, and for more complex pages, I'll need to specify arguments that get passed to the server, like when you enter your username and password. In the back of my mind, I want to keep this all very simple, since the code is ultimately going to be driven by non-technical Customers.

Ok, so I will name Pages. I pass a name into the Page constructor. I compile, and it fails, so I hop over to the Page code, and add a Name to the constructor.

I write this:

1   public class Page {
2     private String name;
3    
4     public Page(String name) {
5       this.name = name;
6     }
7    
8     public String toString() {
9       return name;
10     }
11   }

Then I modify the test to simply pass in a name: Page page = new Page("HelloServlet");

I compile the Page and test code and run it and all is good. But then I have this moment of guilt. Since I type pretty fast, I threw in the toString() method on Page, when I didn't really need it. XP has this expression which says You Aren't Gonna Need It (YAGNI). Sure, a toString() method can be useful, but frankly, I do not need it now so why have I written it? I go back and delete it toString() from Page.java.

Now, I decide to make some bigger steps. I want to get the contents of a Page and then get a reference to that content to see that it is not null. So I write more test code. I add this method to PageUnitTest:

1     public void testPageRetrieval() {
2       Page page = new Page("HelloServlet");
3       page.retrieve();
4       String contents = page.contents();
5       assertNotNull(contents);
6     }

Now, this doesn't compile, so I have to go add methods retrieve() and contents() to Page.java. I do that, writing almost no code accept for the declarations, and I have contents() return null:

1   public class Page {
2     private String name;
3    
4     public Page(String name) {
5       this.name = name;
6     }
7    
8     public void retrieve() {
9     }
10    
11     public String contents() {
12       return null;
13     }
14   }

Ok, now I compile and run PageUnitTest and it fails, telling me:

..F
Time: 0.0

FAILURES!!!
Test Results:
Run: 2 Failures: 1 Errors: 0
There was 1 failure:
1) PageUnitTest$2.testPageRetrieval
junit.framework.AssertionFailedError
	at junit.framework.TestCase.fail(TestCase.java:233)
	at junit.framework.TestCase.assert(TestCase.java:95)
	at junit.framework.TestCase.assertNotNull(TestCase.java:174)
	at junit.framework.TestCase.assertNotNull(TestCase.java:168)
	at PageUnitTest.testPageRetrieval(PageUnitTest.java:41)
	at PageUnitTest$2.runTest(PageUnitTest.java:24)
	at junit.framework.TestCase.runBare(TestCase.java:299)
	at junit.framework.TestResult.run(TestResult.java:66)
	at junit.framework.TestCase.run(TestCase.java:289)
	at junit.framework.TestSuite.run(TestSuite.java:120)
	at junit.textui.TestRunner.doRun(TestRunner.java:34)
	at junit.textui.TestRunner.start(TestRunner.java:131)
	at junit.textui.TestRunner.main(TestRunner.java:57)

Ok, what now? Well, I haven't done anything real yet. Time to do something real. For starters, I'll need a URL to use so that I can go get some content. I need to pass this to Page, and the first thought is to do it in the constructor, but passing a Name and a URL bugs me - seems redundant, since the name could be used to look up the URL. So I decide to stop thinking so much and just pass it to Page in a set method.

1   import junit.framework.*;
2    
3   public class PageUnitTest extends TestCase {
4    
5     public PageUnitTest(String name) {
6       super(name);
7     }
8    
9     static Test suite() {
10       TestSuite suite= new TestSuite();
11    
12       suite.addTest(
13          new PageUnitTest("testPageCreation") {
14            protected void runTest() {
15              this.testPageCreation();
16            }
17          }
18       );
19    
20       suite.addTest(
21          new PageUnitTest("testPageRetrieval") {
22            protected void runTest() {
23              this.testPageRetrieval();
24            }
25          }
26       );
27    
28       return suite;
29     }
30    
31     public void testPageCreation() {
32       Page page = new Page("HelloServlet");
33       assertNotNull(page);
34     }
35    
36     public void testPageRetrieval() {
37       Page page = new Page("HelloServlet");
38       page.setLocation("http://localhost:8000/servlets/HelloServlet");
39       page.retrieve();
40       assertNotNull(page.contents());
41     }
42   }

After updating this test code, I go back to Page.java to update it with the set method. Nothing complicated at this point so I'll keep going.

Now I want to really make Page.java do something, so I can see if my test works. I want a Page instance to be able to go get some content from a real web page. Here's what I add:

1   import java.net.*;
2   import java.io.*;
3    
4   public class Page {
5     private String name;
6     private String location;
7     private String contents;
8     private URL url;
9    
10     public Page(String name) {
11       this.name = name;
12     }
13    
14     public void setLocation(String location) {
15       this.location = location;
16     }
17    
18     protected BufferedReader createReader() {
19       BufferedReader reader = null;
20       try {
21         URL url = new URL(location);
22         reader = new BufferedReader(new InputStreamReader(url.openStream()));
23       } catch (Exception ignored) {
24         System.err.println(e);
25       }
26       return reader;
27     }
28    
29     public void retrieve() {
30       contents = null;
31       BufferedReader reader = createReader();
32       if (reader == null) return;
33       String line;
34       try {
35         while ((line = reader.readLine()) != null)
36           contents += line;
37       } catch (Exception e) {
38         System.err.println(e);
39       } finally {
40         try {reader.close();} catch (Exception ignored) {}
41       }
42     }
43    
44     public String contents() {
45       return contents;
46     }
47   }

There is no need to change the test code at this point. So I run it against this new version of Page. But before I can run the test, I have to kick-off JRun, which is responsible for serving up my Servlet, HelloServlet. I kick off JRun, and I'm ready. I'm now expecting the test to work, but I'm surprised to find that it does not!

As you'll see in the paragraphs below, I find the solution to this problem. But, as I'm re-reading what I wrote, I realize that I missed an opportunity. It would've been good to test the software before kicking off JRun. That might have given me good feedback to use to either make my test better or just to ensure that it even runs when JRun isn't loaded. This is a good lesson for me.

On further inspection, I see that a File Not Found exception is being thrown because the URL I passed to Page.java was wrong -- I wrote "servlets" instead of "servlet". Ok, no big deal, I fix the URL and pass both of the tests in PageUnitTest. But this error gets me thinking. Perhaps I need a test to confirm when a URL is bad? I set out to write that test. When I'm done I run the tests again - and now all three pass. Here's the latest test code:

1   import junit.framework.*;
2    
3   public class PageUnitTest extends TestCase {
4    
5     public PageUnitTest(String name) {
6       super(name);
7     }
8    
9     static Test suite() {
10       TestSuite suite= new TestSuite();
11    
12       suite.addTest(
13          new PageUnitTest("testPageCreation") {
14            protected void runTest() {
15              this.testPageCreation();
16            }
17          }
18       );
19    
20       suite.addTest(
21          new PageUnitTest("testPageRetrieval") {
22            protected void runTest() {
23              this.testPageRetrieval();
24            }
25          }
26       );
27    
28       suite.addTest(
29          new PageUnitTest("testBadURL") {
30            protected void runTest() {
31              this.testBadURL();
32            }
33          }
34       );
35    
36       return suite;
37     }
38    
39     public void testPageCreation() {
40       Page page = new Page("HelloServlet");
41       assertNotNull(page);
42     }
43    
44     public void testPageRetrieval() {
45       Page page = new Page("HelloServlet");
46       page.setLocation("http://localhost:8000/servlet/HelloServlet");
47       page.retrieve();
48       assertNotNull(page.contents());
49     }
50    
51     public void testBadURL() {
52       Page page = new Page("HelloServlet");
53       page.setLocation("http://localhost:8000/servlets/HelloServlet");
54       page.retrieve();
55       assert(page.contents() == null);
56     }
57   }

Ok, now I'm starting to feel a rhythm to this development style. I think about what must come next. What am I currently testing? I'm testing that I can

  • instantiate a page
  • get content back from a page
  • get no content back from a page when a URL is bad
But I'm not testing if the content I do get back is the correct content. Here's where I can now start to code some real Customer-centric functional tests. For I envision a Customer saying, when I submit this web page, this is the content I would expect to get back. Yes, ok, time to write yet another test.

1     public void testPageContents() {
2       Page page = new Page("HelloServlet");
3       page.setLocation("http://localhost:8000/servlet/HelloServlet");
4       page.retrieve();
5       assert(page.contents().indexOf("Hello") > -1);
6     }

testPageContents makes sure that the page that is returned contains the contents I expect. I run the tests, and all of them pass.

Ok, I now feel that I have a pretty good Page class. It is simple and direct. I know that I want to implement the more complicated PageGet and PagePost classes next. And I know that I'll want to add more functionality to Page at some point. But I am keeping my goals in mind and my goals are to be able to test simple Web pages, pages submitted with an HTTP Get, and pages submitted with an HTTP Put.

This will conclude this journey, but check soon for the continuation - we'll build PageGet and PagePost together, and see what becomes of Page.java.

  Industrial Logic, Inc.

 

History Of XP
XProgramming.com
XPlorations

FacebookFacebook  TwitterTwitter  linked inLinkedIn