uild a Java Web Application Using HttpUnit and Test-driven Methodology, Part I showed how HttpUnit complements the JUnit functionality to provide testing capability for Web applications, which ordinarily are difficult to test using only JUnit. It then demonstrated a step-by-step setup of a test-driven environment and a phone list Web application.
Part I concluded with a successful test of the application. To complete it, however, you still have to build the following functions: new contact, delete contacts, and edit contact. You also need to write the tests for those functions before implementing the application’s full functionality. Part II completes the example, performing these tests and filling out the functionality of the phone list application.
Make sure you have started up Tomcat. If you are unsure how to do this, refer back to the end of Part I for step-by-step instructions. This article assumes you have Tomcat running on a local machine (localhost).
The “New Contact” Function
The “new contact” feature is accessible at http://localhost:8080/phonelist/new.do. This URL should bring up a page with a form that accepts entry of a contact’s properties: last name, first name, phone, fax, and email. You also need a save action that does the following:
- Accepts the data entered by the user at the Web browser
- Creates a new ContactBean instance with a unique ID value
- Stores that bean in the ContactDatabase repository.
The NewTest class should test for the following conditions or functionality:
- On the page displayed by the /showList.do action, a link entitled “Create New Contact” points to /new.do.
- On the new contact page, a form with last name, first name, phone, fax, and email fields has an action attribute pointing to /save.do.
- When a contact is submitted with random data for the fields, the newly created data shows up in the page the showList.do action displays. Furthermore, the number of contacts increases by one.
Type the code contained in Listing 1 into a file named NewTest.java in the src/WEB-INF/classes/com/abcinc/phonelist directory.
The NewTest class should look familiar. It is similar to the ShowListTest class from Part I. The NewTest class will perform the three tests specified in Listing 1. Right now, it performs the first of the three tests. You may compare your NewTest.java file with NewTest.java.v1 from the example code archive (phonelist-example-files.tgz). The NewTest.java.v1 file contains helpful comments, so you may wish to look at it even if you intend to use your own NewTest.java file.
For the new test, you also need to add a target to the build.xml file included in the example code. You should copy build.xml.v2 from the example code archive to a file named build.xml in your ~/projects/phonelist directory. Then, add the following target definition below the existing definition of test-showlist:
description="Run new test">
Additionally, you need to change the existing definition of the test target to append the test-new definition as one of the dependencies. Replace your existing definition of the test target with the following:
Your build.xml file should closely resemble the build.xml.v3 file from the example code archive, with the exception of comments and possibly formatting. You could adopt build.xml.v3 as your build.xml file if you were unsure about whether you made the changes described above correctly.
Run the tests by typing “ant test”. The ant test command will run both the showlist test and the new test to ensure that the old functionality is tested. Any changes in functionality have to not only provide the new functionality being tested for but also avoid breaking any old functionality.
The showlist test should still pass. The new test should also pass at this point, but you have not yet finished coding up the two other test cases of the new contact test suite. After you implement the two other test cases, the NewTest test suite should fail because you have not yet implemented the new contact functionality. Implement the two remaining tests so that you can then implement the functionality that those two tests test.
Add the following to the top of the NewTest.java file beneath the other import statements:
import com.meterware.httpunit.SubmitButton;import com.meterware.httpunit.WebForm;import java.util.Random;
Add the following to NewTest.java after the line that reads: private static String newPath = “/phonelist/new.do”:
private static String savePath = "/phonelist/save.do"; private static String randomCharacterSet =
The randomCharacterSet variable is used to generate random strings. The random strings are used to verify that the application is saving new contacts. Also add the following static variable declaration above the declaration of webConversation:
private static Random random = new Random();
Now add the new methods contained in Listing 2 to NewTest.java.
The testNewContactForm method verifies that the new contact form exists, that it contains the fields you require, and that it has the proper action setting. The testSaveNewContact method is a bit more complex. It figures out the number of rows in the list of contacts, then retrieves the new contact form and simulates the process of filling out the form fields with completely random strings of eight characters each for the last name, first name, phone, fax, and email fields. It then simulates a user pressing the “Save” button. Since both a “Save” button and a “Cancel” button exist, it is important to simulate the click on the right button. Finally, it parses the response from saving the contact, which should be the “showList” page. It checks that the number of contacts has increased by one and that one of the contacts in the list matches the newly entered contact.
The getRandomString method is simply a helper method that generates a random string of a length specified by a parameter. The testSaveNewContact method calls on getRandomString in order to generate values for the different fields. The testSaveNewContact method tests the values that were submitted with the updated list of contacts to make sure that the newly appearing contact on the list matches what was submitted. At this point, your NewTest.java file should be the same as NewTest.java.v2 from the example code, with the exception of comments and possibly formatting.
Run ant test now. The showlist test suite should pass completely, but the new test will only partially succeed. Two of the three test cases in the test suite will fail due to the lack of a “/new.do” action. You will receive an error message like the following:
[java] 2) testSaveNewContact(com.abcinc.phonelist.test.NewTest)com.meterware.httpunit.HttpException:
Error on HTTP request: 400 Invalid path /new was requested [http://localhost:8080/phonelist/new.do]
Implement “New Contact” Function
Implement the new contact functionality so that the application will pass the NewTest test suite in addition to the ShowListTest test suite. The files you will need to create or change are:
The NewAction class simply populates the EditForm class with default values and then displays the JSP containing the HTML for the form. Some forms contain default values. In your case, no default values exist.
The EditForm class is a container for shuttling data between the persistence layer (the JavaBean classes that model data in the database) and the presentation layer (the JSPs or HTML forms that display information and accept input from a browser). The reason that Struts form classes are useful is that the persistence layer oftentimes has differences in data type or cardinality from the presentation layer. As a concrete example, consider the case of a date value. In most databases, a date is stored as a single field that contains the month, day, and year. In Java, you store a date in a java.util.Date object. Most people consider it easier to enter dates with three drop-down fields or two drop-down fields and one text field. Month and day are usually drop-down fields. Year is occasionally a drop-down field, but sometimes a text field. Even if you asked the user to enter the date as a single field, you still have to deal with type conversion because standard Web forms can transfer only string data.
The edit.jsp file contains the HTML for the form, including a section at the top for display of input errors and the Struts tags to render HTML input fields. The reason you use edit.jsp rather than creating a separate file called new.jsp is that the form for creating a new contact is exactly the same as the form for editing an existing contact with several exceptions, including the following:
- The new contact form does not have a value for the hidden ID field that identifies the record being edited.
- The new contact form may have a different page title or a different table heading than the edit contact form (e.g., “New Contact” vs. “Edit Contact”).
Make the following changes:
- Copy NewAction.java.v1 from the example code archive to src/WEB-INF/classes/com/abcinc/phonelist and name the file NewAction.java.
- Copy EditForm.java.v1 from the example code to src/WEB-INF/classes/com/abcinc/phonelist and name the file EditForm.java.
- Copy edit.jsp.v1 from the example code to src/web and name the file edit.jsp.
- Add the following section to struts-config.xml in the
section right after the entry for manageForm:
- Add the following sections to struts-config.xml in the
The reason you need to define “/save” right now is that you use “/save” as the action attribute of the HTML form in edit.jsp. Struts requires that action mapping definitions exist before defining HTML forms that refer to those action mappings.
- Add the following section to tiles-defs.xml:
- Copy ApplicationResources.properties.v1 from the example code to src/WEB-INF/classes/com/abcinc/phonelist and name the file ApplicationResources.properties. The ApplicationResources.properties file stores string resources for Struts, primarily the labels of fields and error messages. Having all of the labels, error messages, and other visible text in a file makes it easy to internationalize the application. In order to create a version in Russian or French or German for example, all you have to do is translate the text and create a separate ApplicationResources.properties file (with a suffix denoting the locale). The Web application will automatically use the appropriate file based on the locale.
At this point, your struts-config.xml file should be equivalent to struts-config.xml.v3 from the example files, except for formatting and comments. Your tiles-defs.xml file should be equivalent to tiles-defs.xml.v3, except for formatting and comments.
Now perform the following steps:
- Shut down Tomcat by typing ./shutdown.sh in the bin subdirectory of the Tomcat distribution directory.
- Run ant undeploy. If you don’t run ant undeploy, the unpacked directory tree for the phonelist application remains in the Tomcat deploy directory. Consequently, when Tomcat restarts it does not bother unpacking the revised version of the phonelist application from the phonelist.war archive. Thus, it will appear that the application never changes.
- Run ant deploy.
- Start up Tomcat by typing ./startup.sh in the bin subdirectory of the Tomcat distribution directory.
Perform the new tests by typing ant test. Most of your tests should pass, with the exception of the testSaveNewContact test case in the NewTest test suite. This is natural since you didn’t implement the SaveAction. Testing is designed to catch goofs like this.
Go back and remedy the problem by performing the following steps:
- Copy SaveAction.java.v1 from the example code to src/WEB-INF/classes/com/abcinc/phonelist and name the file SaveAction.java.
- Copy validation.xml.v2 from the example code to src/WEB-INF and name the file validation.xml, replacing your existing version of validation.xml. Although not necessary, this helps with checking input in the new/edit contact form to ensure that proper values are entered. The validation at this point consists solely of mandating that the first and last names are entered.
Now re-run the tests by typing ant test in the phonelist project directory. All of the tests should pass.
The “Delete Contacts” Function
You are ready to move on to building the tests for the remainder of the functionality: delete and edit. The delete functionality will be nice. Right now the testSaveNewContact test case of the NewTest test suite is messy. It creates a new contact, but does not delete the contact afterwards. The edit functionality should be easy to test because it will look very similar to the tests for the new functionality. Implement the delete functionality first by performing the following steps:
- Copy DeleteTest.java.v1 from the example code archive to the src/test/com/abcinc/phonelist/test directory and name the file DeleteTest.java.
- Copy build.xml.v4 over your existing build.xml. The new version of build.xml has the following changes: (1) the addition of a new target named test-delete; (2) the addition of test-delete to the test target as a dependency; (3) the addition of the Web application classes to the classpath for the tests so that the tests can pick up classes like ContactBean; (4) the addition of compile-classes to the compile-tests target as a dependency in order for the tests to make use of the compiled classes from the Web application; and (5) the change of the compile-tests target to add the Web application classes to the classpath for compilation of the tests.
- Copy ContactBean.java.v2 from the example code to the src/WEB-INF/classes/ com/abcinc/phonelist directory and name the file ContactBean.java, in place of your existing copy of ContactBean.java. You added a new “equals” method that allows you to compare two ContactBean instances for equality of all fields, which is used in DeleteTest.java.
Type ant test to verify that although the NewTest and ShowListTest test suites pass, the DeleteTest test suite fails one test. The check for the presence of the delete button passes, but the check to verify that the delete action works does not pass. You should get an error like the following:
[java] There was 1 error: [java] 1) testDeleteContact(com.abcinc.phonelist.test.DeleteTest)com.meterware.httpunit.HttpInternalErrorException:
Error on HTTP request: 500 Internal Error [http://localhost:8080/phonelist/delete.do]
If your test suite does not fail, it usually means you made an error in coding the test. Take the following step to implement the delete functionality so that your test will pass:
- Copy DeleteAction.java.v1 from the example code archive to src/WEB-INF/classes/com/abcinc/phonelist and name the file DeleteAction.java.
Ordinarily, you need to add a section to struts-config.xml for every action, but you already added a section for the “/delete” action. You added it in Part I in order to show the list of contacts with a form that allowed for selecting items for deletion. You may recall that Struts requires the definition of actions used in forms, so even though you had not yet implemented the “/delete” action, you still had to have the definition in struts-config.xml in order to display the form in the showList.jsp file.
Try the tests again. Make sure you shut down Tomcat, undeploy the application, deploy the application, and then start up Tomcat. The DeleteTest test suite should now pass all tests. A good thing to do now is update the NewTest test suite so that it cleans up after itself. Now that you have the delete function working properly, you can delete the new contact you created in NewTest to test the ability to create and save new contacts.
Replace your version of NewTest.java in src/test/com/abcinc/phonelist/test with NewTest.java.v3 from the example code. Make sure you keep the NewTest.java filename. If you examine the file, you will see that the testSaveNewContact method contains extra code to determine the ID value of the newly created contact and then submit the form on the showList page with the correct checkbox selected to the delete action.
Run ant clean to be sure that the build directory is clean, then run ant test again to verify that the NewTest test suite works properly and that it no longer leaves a new contact in the database every time it runs. You can point your browser at http://localhost:8080/phonelist/showList.do to verify that the list of contacts does not change after NewTest runs. Replace localhost with whatever machine Tomcat is running on.
The “Edit Contact” Function
The last remaining piece of functionality to write is the “edit contact” functionality. First, write the test. Copy the EditTest.java.v1 file from the example code archive to src/test/com/abcinc/phonelist/test and name it EditTest.java. You will also need to make changes to build.xml, so copy build.xml.v5 from the example code and use it to replace your current build.xml file. The changes you made to build.xml between v4 and v5 were:
- Modified the test target to include test-edit as a dependency
- Added a test-edit target that executes the EditTest class
Run the tests again. You should get the following error:
[java] 1) testEditContact(com.abcinc.phonelist.test.EditTest)com.meterware.httpunit.HttpException:
Error on HTTP request: 400 Invalid path /edit was requested [http://localhost:8080/phonelist/edit.do?id=6]
Now implement the “edit contact” functionality so that the EditTest suite will pass. Copy the EditAction.java.v1 file from the example code to src/WEB-INF/classes/com/abcinc/phonelist. Name the file EditAction.java. You also need to add a section to struts-config.xml. You can simply copy struts-config.xml.v4 from the example code over your existing struts-config.xml file in src/WEB-INF. The one change in struts-config.xml between v3 and v4 is that you added an action definition for the “/edit” action.
After redeploying (shut down Tomcat, undeploy, deploy, start up Tomcat), run the tests again. Your Web application should now pass all of the tests.
Test Your Skills After Testing the App
Try the following challenges to flex your skills in test-driven development:
- Add code to the testSaveNewContact method in NewTest that verifies that the “Cancel” button works properly on the “new contact” action. Although you perform a check to ensure that “Cancel” works for the “edit contact” action, you make no such check for the “new contact” action. If it works properly, you should be able to enter information in the fields of the “new contact” form, then click on “Cancel” and have the list of contacts not increase by one.
- Add a test to EditTest that verifies whether the validation is working properly. You could try programmatically posting the “edit contact” form with a blank last name or a blank first name.
- Rewrite the application to interface with a relational database instead of the in-memory storage you devised in ContactDatabase. This should simply involve changing ContactDatabase.java. Your tests will be useful to ensure that none of the functionality has been broken in the port over to a relational database for the backend data store.