Tutorial
Task 1
Step 4: Validating data entry
In this step, you will learn how to:
• Access a servlet directly during an invocation
• Extract a table from a web response, using its contents
• Examine the contents of an HTML table
• Verify that fields are marked read-only
We are nearly done with the pool editor. We can now use it to define the contents of the pool; however, we can only permit the administrator to open the pool for betting if it is valid. We therefore have to define the validity rules. For this tutorial, the only rules that we will insist on is that all teams must have opponents, and that only a game with a pair of teams may be selected as the tie-breaker.
We must also prevent edits to the pool once it has been opened.
Validation is often complicated, since we have to not only check the data against our validation rules, we also have
to recognize the need to validate, and modify the output to show any errors. It would be nice if we could break this into
pieces and build one at a time. With ServletUnit
, we can:
public void testPoolValidation() throws Exception { ServletRunner sr = new ServletRunner( "web.xml" ); ServletUnitClient client = sr.newClient(); client.setAuthorization( "aUser", "pool-admin" ); WebResponse response = client.getResponse( "http://localhost/PoolEditor" ); WebForm form = response.getFormWithID( "pool" ); form.setParameter( "away1", "Detroit Lions" ); form.setParameter( "home1", "Denver Broncos" ); form.setParameter( "home2", "Baltimore Ravens" ); form.setParameter( "tiebreaker", "3" ); WebRequest request = form.getRequest( "save", "Open Pool" ); // (1) select the request object directly InvocationContext context = client.newInvocation( request ); // (2) create an invocation context PoolEditorServlet servlet = (PoolEditorServlet) context.getServlet(); // (3) locate the invoked servlet servlet.updateBettingPool( context.getRequest() ); // (4) ask servlet to update the data String[] errors = servlet.getValidationErrors(); // (5) ask servlet to check the data assertEquals( "Number of errors reported", 2, errors.length ); assertEquals( "First error", "Tiebreaker is not a valid game", errors[0] ); assertEquals( "Second error", "Game 2 has no away team", errors[1] ); }
This test starts out like all the others, but once we have created the request, things venture into new territory:
updateBettingPool
method (which we need to make package-accessible), passing the request
object found in the context.This test won't even compile yet, so before proceeding, we should create the new method in the servlet without any logic:
String[] getValidationErrors() { return new String[0]; }
Now it compiles and fails, as expected. To make this test pass, we need to implement the new method in the servlet:
String[] getValidationErrors() { ArrayList errorList = new ArrayList(); BettingPoolGame game = BettingPool.getGames()[ BettingPool.getTieBreakerIndex() ]; if (game.getAwayTeam().length() == 0 || game.getHomeTeam().length() == 0) { errorList.add( "Tiebreaker is not a valid game" ); } BettingPoolGame[] games = BettingPool.getGames(); for (int i = 0; i < games.length; i++) { if (games[i].getAwayTeam().length() == 0 && games[i].getHomeTeam().length() != 0) { errorList.add( "Game " + i + " has no away team" ); } else if (games[i].getAwayTeam().length() != 0 && games[i].getHomeTeam().length() == 0) { errorList.add( "Game " + i + " has no home team" ); } } String[] errors = (String[]) errorList.toArray( new String[ errorList.size() ] ); return errors; }
Once we are sure of our validation logic, we need to have the error messages displayed. We will arrange to have any error message displayed in the top of row of the table, and we will highlight any cells containing bad inputs. We therefore ask for the response from the bad open pool request:
public void testBadPoolOpen() throws Exception { ServletRunner sr = new ServletRunner( "web.xml" ); ServletUnitClient client = sr.newClient(); client.setAuthorization( "aUser", "pool-admin" ); WebResponse response = client.getResponse( "http://localhost/PoolEditor" ); WebForm form = response.getFormWithID( "pool" ); form.setParameter( "away1", "Detroit Lions" ); // (1) enter bad values into the form form.setParameter( "home1", "Denver Broncos" ); form.setParameter( "home2", "Baltimore Ravens" ); form.setParameter( "tiebreaker", "3" ); SubmitButton openButton = form.getSubmitButton( "save", "Open Pool" ); // (2) select the desired submit button response = form.submit( saveButton ); // (3) submit the form WebTable errorTable = response.getTableWithID( "errors" ); // (4) Look for the error table assertNotNull( "No errors reported", errorTable ); errorTable.purgeEmptyCells(); // (5) Remove any empty cells from the table String[][] cells = errorTable.asText(); // (6) Convert non-empty cells to text assertEquals( "Number of error messages provided", 2, cells.length - 1 ); assertEquals( "Error message", "Tiebreaker is not a valid game", cells[1][0] ); assertEquals( "Error message", "Game 2 has no away team", cells[2][0] ); }
Note:
This test passes once we modify the end of the doPost
method :
pw.println( "<html><head></head><body>" ); if (request.getParameter( "save" ).equals( "Open Pool" )) { String[] errors = getValidationErrors(); if (errors.length != 0) reportErrors( pw, errors ); } printBody( pw ); pw.println( "</body></html>" ); } private void reportErrors( PrintWriter pw, String[] errors ) { pw.println( "<table id='errors' width='90%' style='background-color=yellow; " ); pw.println( " border-color: black; border-width: 2; border-style: solid'>" ); pw.println( "<tr><td colspan='2'><b>Cannot open pool for betting:</b></td></tr>" ); for (int i=0; i < errors.length; i++) { pw.println( "<tr><td width='5'> </td><td>" + errors[i] + "</td></tr>" ); } pw.println( "</table>" ); }
Note that we are actually displaying two cells for each error. The first is blank, and is simply used for formatting, as many web designers tend to do. The test code will ignore this, so that if the page is later modified to use stylesheets to control its formatting, the test will be unaffected. For this same reason, the tests in this tutorial tend to ignore formatting issues in general, and only look at structural elements.
If everything is valid, we should be able close the pool. This will be reflected by a change in state of the BettingPool object - which will later be used to change the options available to the users - and should forbid future changes to the pool itself. We will test this by verifying that the "save" submit buttons are no longer enabled:
public void testGoodPoolOpen() throws Exception { ServletRunner sr = new ServletRunner( "web.xml" ); ServletUnitClient client = sr.newClient(); client.setAuthorization( "aUser", "pool-admin" ); WebResponse response = client.getResponse( "http://localhost/PoolEditor" ); WebForm form = response.getFormWithID( "pool" ); form.setParameter( "away1", "Detroit Lions" ); form.setParameter( "home1", "Denver Broncos" ); form.setParameter( "away3", "Indianapolis Colts" ); form.setParameter( "home3", "Baltimore Ravens" ); form.setParameter( "tiebreaker", "3" ); form.getSubmitButton( "save", "Open Pool" ).click(); // (1) click the submit button response = client.getResponse( "http://localhost/PoolEditor" ); // (2) retrieve the page separately form = response.getFormWithID( "pool" ); assertNull( "Could still update the pool", form.getSubmitButton( "save" ) ); // (3) look for the buttons try { WebRequest request = form.getRequest(); request.setParameter( "home3", "Philadelphia Eagles" ); // (4) try to change an entry fail( "Could still edit the pool" ); } catch (IllegalRequestParameterException e) {} }
Note:
We have to make changes in two places to make this behavior work. The following code change to printBody
makes the form display read-only once the pool is open:
for (int i = 0; i < games.length; i++) { pw.println( "<tr><td>" ); pw.print( "<input name='home" + i + "' value='" + games[i].getHomeTeam() + "'" ); pw.println( getReadOnlyFlag() + "></td>" ); pw.print( "<td><input name='away" + i + "' value='" + games[i].getAwayTeam() + "'" ); pw.println( getReadOnlyFlag() + "></td>" ); pw.print( "<td><input type='radio' name='tiebreaker' value='" + i + "'" + getReadOnlyFlag() ); if (i == BettingPool.getTieBreakerIndex()) pw.print( " checked" ); pw.println( " /></td></tr>" ); } pw.println( "</table>" ); if (BettingPool.isEditable()) { pw.println( "<input type='submit' name='save' value='Save' />" ); pw.println( "<input type='submit' name='save' value='Open Pool' />" ); } pw.println( "</form>" ); } private String getReadOnlyFlag() { return BettingPool.isEditable() ? "" : " readonly"; }
and we have to make a small change to doPost
in order to mark the pool open:
String[] errors = getValidationErrors(); if (errors.length != 0) reportErrors( pw, errors ); else { BettingPool.openPool(); } }
The pool editor is now complete. In the next task, you will address access to the application.