JaVa
   

HttpUnit Basics

HttpUnit can be thought of as two clusters of functionality:

The first cluster of functionality allows for the basics of talking to a Web server and is easily explained. The second cluster allows for the unit testing of individual pages. Used together, the clusters can verify complex behavior such as page flow. The rest of this section will explain how to employ the basics of HttpUnit.

WebClient Functionality

HttpUnit's WebClient class, appropriately enough, models a Web client. (Actually, WebClient is an abstract class; WebConversation provides the implementation that test developers will use.) WebClient/WebConversation acts basically like a standard Web browser: It maintains client state—including persistent response headers such as cookies, relative URLs, and frame sets—and allows a user to send requests for specific resources and retrieve the responses. Natural collaborators with WebConversation are WebRequest and WebResponse. WebRequest (naturally) represents a request to a remote server, and WebResponse encapsulates the reply. Using these classes is simple:

WebConversation conversation = new WebConversation();
WebRequest simpleRequest =
 new GetMethodWebRequest("http://httpunit.sf.net/");
WebResponse response = conversation.getResponse(simpleRequest);


Note that GetMethodWebRequest is a subclass of the abstract class WebRequest (specifically one that uses HTTP GET). The last two lines occur so often that WebConversation implements a convenience method to shorten them into one line:

WebResponse response =
 conversation.getResponse("http://httpunit.sf.net/");


Building on these core classes, HttpUnit provides the facility to follow links contained in WebResponses:

WebLink link = response.getLinkWith("automated web site testing");
WebRequest request = link.getRequest();
response = conversation.getResponse(request);


Note that the text searched for in getLinkWith is the clickable text, not the value of the "href" attribute. Also, getLinkWith will return the first link that contains the matching text. HttpUnit provides other methods for link searching via its API.

Response Inspection and Multiple Pages

Requesting and retrieving pages is great—but how do you use HttpUnit to verify that the responses are in order? Several methods on the WebResponse object are designed to allow easy inspection of the response. These run the gamut from the quick and dirty (return the response as text with response.getText()) to the specific (find a table with response.getTableWithSummary()) to the ultra- powerful (get the XML DOM for the Html page with response.getDOM()). The results of these methods can be used in combination with JUnit assertions to check that the requested pages are as expected. The following example introduces a fully working functional Web test. You can download the JSPs and HTML files that serve as the app under test, as well as the test itself, from the tutorial's Web site at www.RickHightower.com/JavaXP. The folder on this chapter contains a short read-me file describing how to download and set up the example code. The component under test is the annual sales report page (presumably generated dynamically from the database). Because sales reports are naturally sensitive matter, a login page protects the page. To test this component, the test developer might decide that the following facts should be verified:

It's easy to see how many different testable facts can be contained in a relatively simple set of user actions. This is only natural, and it makes thorough functional testing somewhat daunting. Still, the task of functional testing with HttpUnit can be made easier with good design. The first step is to try to break down the testable facts into small groups that can be unit tested. For this example, the entire test will be contained inside a JUnit TestCase (see , “Unit Testing with JUnit,” if you are unfamiliar with its functionality). Each of the groups of facts will be verified with its own test method—for example, testLogin(). The setUp() method will initialize the instance variable conversation with a new WebConversation to avoid having to create one in each test method.

Login Testing

The test method for the login page will verify the testable fact summarized under "Login" in the previous section. The test simulates the following page flow: A user attempts to access a private page. She is redirected to the login page and fills it out incorrectly. Her login is processed, and the app takes her back to the login page, displaying an error message. Finally she logs in correctly and arrives at the private page. The following listing contains the code for login testing.

public void setUp(){
 conversation = new WebConversation();
}
private WebResponse goToPrivatePage() throws Exception{
 /*try to get to a protected page*/
 return conversation.getResponse(
 "http://localhost:8080/examples/private.jsp");
}
public void testLogin() throws Exception{ WebResponse response = this.goToPrivatePage();
 /*verify that we are at the login page*/
 assertLoginPage(response);
 /*try to login with a bad username and password*/
 response = login(response, "xxx", "notAPassword");
 assertLoginPage(response);
 /*check that an error message has been displayed*/
 String pageStr = response.getText();
 assertTrue(pageStr.indexOf("Password or User name not correct.")>-1);
 /*login correctly*/
 response = login(response, "user", "muffinWaste77");
 assertTrue(!response.getTitle().equals("Login"));
}
public void assertLoginPage(WebResponse response)throws Exception{
 /*
 page title is often a quick verification that the correct page is
 being examined.
 */
 assertEquals("redirected successfully to login",
 "Login", response.getTitle()); }
public WebResponse login(WebResponse loginPage, String userName,
 String pass) throws Exception{
 /* since the forms are not named, get the first form on the page */
 WebForm form = loginPage.getForms()[0];
 /*Get the request from the form*/
 WebRequest loginRequest = form.getRequest();
 /*set the user name and password*/
 loginRequest.setParameter("userName", userName);
 loginRequest.setParameter("password", pass);
 /*return the response for the login*/
 return conversation.getResponse(loginRequest);
}


Notice that this listing breaks the code into four separate methods (in addition to setUp). login() and goToPrivatePage() were originally inlined. After we began work on the other two tests (which needed similar tasks performed), we devised separate methods to avoid code duplication (one of the cardinal rules of code design in XP). Eventually, as the testing app grew, methods such as these would probably find a home in a separate utility class. Another note on design: Almost all the methods in this TestCase declare Exception in the throws clause. HttpUnit classes declare some exceptions that are somewhat difficult to deal with (SAXException, IOException, and MalformedURLException). In most cases there is nothing that a test can do about the underlying cause, aside from bringing it to a developer's attention. Therefore, it makes sense to allow the JUnit framework to handle and log the errors as test failures once they bubble up past the TestCase. Later on, if one of the exceptions becomes meaningful, this behavior can be changed; but for now it makes sense to throw most exceptions through our TestCase. The first method, goToPrivatePage(), uses HttpUnit functionality covered earlier in the chapter and serves mostly to save some tying. assertLoginPage() simply asserts that the page title retrieved with reponse.getTitle() is equal to "Login". These utility methods (along with login()) support testLogin(), which simulates requesting a protected page, logging in incorrectly, and then logging in correctly and being directed to the originally requested resource. The login() method submits a login request and introduces form handling in HttpUnit. The first step is to obtain a WebForm from the WebResponse that simulates the login page. This is taken care of with:

WebForm form = loginPage.getForms()[0];


getForms() returns an array, and we select its first member, corresponding to the first form on the page. WebForm supports the verification of its contents (which we will cover later in this example), but in order to submit it, we first have to get a request that corresponds to a submission of this form:

WebRequest loginRequest = form.getRequest();


Now, form parameters for the username and password are set in the request:

loginRequest.setParameter("userName", userName);


Finally, the request is submitted to the server with conversation.getResponse(request). The testLogin() method contains barely any new HttpUnit code. It uses the utility methods to simulate page flow, and uses JUnit asserts to verify that the results are correct. The only new feature employed is response.getText(), which returns a String equivalent to what View Source would return for the same page in a Web browser. A quick way to check for simple HTML out put is to use String.indexOf() to search the returned text for the specified content:

String pageStr = response.getText();
assertTrue(pageStr.indexOf("Password or User name not correct.")>-1);


Testing the Sales Report Page

Now that the login feature has been tested, we will dive into table manipulation with HttpUnit. The sales report page is laid out (minus the feedback form at the bottom) in one top-level table with two rows (see the following figure). The first row contains two other tables, each contained in a cell, that display the report information. We also know that the sales report page has a complex table layout based on figures retrieved from the company database. This means that we cannot assert the specific textual contents of the page, only its general form. We begin by examining the output of the sales report page contained in the following listing.

Java Click To expand
<!-- head and body tags skipped -->
<table border='true' cellspacing='10' cellpadding='5'>
<caption>
Sales Report vs. forecast
</caption>
<tr>
 <td width="50%">
 <table border='true' title='Sales Report'>
 <caption>
 Sales Report
 </caption>
 <tr>
 <td width='40%'>
 annual sales for fiscal 2004
 </td>
 <td width="20%">
 $99,000,000.00
 </td>
 </tr>
 <tr>
 <td width='40%'>
 annual sales for fiscal 2005
 </td>
 <td width='20%'>
 $300,000,000.00
 </td>
 </tr>
 </table>
 </td>
 <td width="50%">
 <table title='forecast' border="true">
 <caption>
 forecast
 </caption>
 <tr>
 <td width='40%'>
 annual sales for fiscal 2004
 </td>
 <td width='20%'>
 $50,000,000.00
 </td>
 </tr>
 <tr>
 <td width='40%'>
 annual sales for fiscal 2005
 </td>
 <td width='20%'>
 $100,000,000.00
 </td>
 </tr>
 </table>
 </td>
</tr>
<tr>
 <td colspan='2'>
 Board Members <br />
 <ol title='Board Members'>
 <li>Harry Truman</li>
 <li>Harry Potter</li>
 <li>Morgan Fairchild</li>
 <li>Tinky Winky</li>
 <li>Poo</li>
 </ol>
 <br/>
 Note that the actual sales are a lot higher than the projected sales.
 </td>
</tr>
</table>
<!--Feedback form follows-->


HttpUnit recognizes the importance of tables as a page-structuring tool and gives you several handy tools for interacting with them. This example uses verifies three facts:

In order to test the first of these, we need to access each table individually. The WebResponse class has several methods for retrieving tables:

public WebTable[] getTables()
public WebTable getTableStartingWith(String text)
public WebTable getTableStartingWithPrefix(String text)
public WebTable getTableWithID(String text)
public WebTable getTableWithSummary(String text)


Of these, only getTables() will return an array of the top-level tables on this page. This seems like a good way to narrow the results. However, the getTableWithXXX() methods will recurse into tables in search of a match. It would be nice to use one of those methods. The getTableStartingWithXXX() methods return tables based on the text of their first (non-empty) cell. These methods do not suit our needs because the two tables begin identically ("Sales report", and "forecast" are contained in the <caption> tag). The other two methods will search for a table based on the "id" and "summary" attributes of the table. However, upon examination of the HTML output, the page designers have included neither attribute in their tables. According to XP's principle of common code ownership, we should change the class that generates this page. If the tables are summarized or given IDs, they are easier to test. In the real world, we would consider this refactoring. For the purposes of our example, we will leave the page design alone in order to explore more of HttpUnit's functionality. Because the page includes only one top-level table, we go ahead and get it:

WebTable topLevelTable = response.getTables()[0];


WebTable represents an HTML table and has a method that returns a specific cell: getTableCell(int row, int column). We use this to get the first cell of the first row (which contains the sales report table):

TableCell reportCell = topLevelTable.getTableCell(0,0);


TableCell is in turn an object representation of an HTML table cell. It shares many of the methods of WebResponse (in fact, they share an interface, HTMLSegment), including the ability to retrieve contained tables. At this point we want to verify that we have two distinct tables, the report and the forecast. Examining the HMTL, we see that the tables are identified only by their captions. Again, if we had summaries or IDs, we could make short work of this problem. However, none exist, and we have no quick way to determine a specific subtag (we could access the HTML DOM, but let's leave that for later in the chapter). So, we decide again to use a quick substring search that verifies the caption exists in the cell at all:

String cellStr = cell.asText();
assertTrue(cellStr+ "contained " +caption,cellStr.indexOf(caption)>-1);


Now that we have checked to be sure we are seeing the cell containing the correct table, we can get the actual cell and verify its structure:

WebTable table = cell.getTables()[0];
this.assertGenericSalesTable(table);//a custom method


The assertGenericSalesTable() method uses a for loop to iterate over the contents of one of the sales tables. It also uses WebTable.asText(), which returns the table as a two-dimensional String array. (This is only a convenience, because the same result can be obtained with TableCell.asText()—in fact, WebTable uses TableCell.asText() internally.) Each row in the table is checked—the first cell must start with "annual sales for fiscal ", and the second cell must begin with a dollar sign. The following listing contains the full code for this portion of the test.

/*
 Sets up the sales report page to be checked by
 assertSalesReportPage(WebResponse response)
 */
public void testReportsPage() throws Exception{
 WebResponse response = this.goToPrivatePage();
 /*redirected automatically to login page, so attempt to log in*/
 response = login(response, "user", "muffinWaste77");
 /*now we should be on the sales report page*/
 assertSalesReportPage(response);
}
public void assertSalesReportPage(WebResponse response) throws Exception{
 /*will return null--designers have unhelpfully forgotten to specify 'summary' attribute!
 */
 //response.getTableWithSummary("Sales Report"); 
 /*
 Also unusable--both the "Forecast" and "Sales Report" tables start with the same cell!
 */
 //response.getTableStartingWith("Annual sales for fiscal 2004");
 WebTable topLevelTable = response.getTables()[0];
 TableCell reportCell = topLevelTable.getTableCell(0,0);
 assertSalesCell("Sales Report", reportCell);
 //WebTable reportTable = reportCell.getTables()[0];
 TableCell forecastCell = topLevelTable.getTableCell(0,1);
 assertSalesCell("Forecast", forecastCell);
 TableCell boardCell = topLevelTable.getTableCell(1,1);
 assertBoardCell(boardCell);
}
public void assertSalesCell(String caption, TableCell cell)throws Exception{
 /*verify the cell contains the caption*/
 String cellStr = cell.asText();
 assertTrue(cellStr+ "contained " +caption,cellStr.indexOf(caption)>-1);
 /*get the table and verify its structure*/
 WebTable table = cell.getTables()[0];
 this.assertGenericSalesTable(table);
}
public void assertGenericSalesTable(WebTable table) throws Exception{
 String[][] tableArray = table.asText();
 for(int i =0; i< tableArray.length; i++){
 assertTrue("row correctly captioned", tableArray[i][0].startsWith("annual sales for fiscal "));
 assertTrue("row contains dollar figure", tableArray[i][1].startsWith("$"));
 }
}
public void assertBoardCell(TableCell cell) throws Exception{
 /*should span 2 columns*/
 assertEquals(2, cell.getColSpan());
 /*
 Turn the cell into text and verify that it contains "Board Members"
 */
 String cellStr = cell.asText();
 assertTrue(cellStr.indexOf("Board Members ") > -1);
}


At this point, the sales report page is all but verified. Verifying that the board member list exists involves only another substring search through a text rendering of a cell. All that remains in this TestCase is to check on the feedback form at the bottom of the page:

Java Click To expand

To complete our functional test of this component of the Web app, we have to check that the form at the bottom of the page is structured correctly and that it will accept average user input. The page designers have named this form so that it can be retrieved with this simple code:

WebForm feedbackForm = response.getFormWithName("feedback");


WebForm (naturally) represents an HTML form. If we examine the page's HTML, we can see that this form is laid out internally using a table—this layout should not affect anything we are doing, because WebForm models the form itself, not the exact HTML on which it is based. The first check we can run is that the form contains all of the possible parameters we expect. First we retrieve the possible parameters with:

String[] parameters = feedbackForm.getParameterNames();


Then we can check these versus our expectations with a custom assert function:

String[] expected = {"name", "email", "reply", "usefulness"};
assertArraysEqual(expected, parameters);


Now we can verify that the "usefulness" dropdown box contains the correct parameters using a similar technique:

String[] usefulnessValues = feedbackForm.getOptionValues("usefulness");
String[] expectedValues = {"1","2","3"};
assertArraysEqual(expectedValues, usefulnessValues);


We can also assert that a given parameter has a default value:

assertEquals("2", feedbackForm.getParameterValue("usefulness"));


Once we have checked on the state of the form in the received page, we can fill it out and submit it. We have already covered the basics of form submission with the login page; only a couple of wrinkles remain undiscussed. First, all form parameters are set with request.setParameter(). Parameters that accept more than one value (multiple select boxes, for instance) can be set with request .setParameter(String name, String[] values). By default, parameters are checked against the acceptable values in the underlying form. Attempting to set the "usefulness" parameter to "600" would yield an IllegalRequestParameterException. Checkboxes and other request parameters can be removed from the request with request.removeParameter(String name). Note that HttpUnit does not check whether you are removing a parameter that could not be removed from the browser. The statement

feedbackRequest.removeParameter("usefulness");


would be just fine by HttpUnit. Once all the request parameters are set, the request can be submitted in the usual manner.


JaVa
   
Comments