Tutorial -> Task 1 -> Step 4: Validating data entry

Testing servlet internals

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.

Testing bad inputs

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" );
    WebRequest request = form.getRequest( "save", "Open Pool" );

    request.setParameter( "away1", "Detroit Lions" );
    request.setParameter( "home1", "Denver Broncos" );
    request.setParameter( "home2", "Baltimore Ravens" );
    request.setParameter( "tiebreaker", "3" );
    InvocationContext context = client.newInvocation( request );           // (1) create an invocation context

    PoolEditorServlet servlet = (PoolEditorServlet) context.getServlet();  // (2) locate the invoked servlet
    servlet.updateBettingPool( context.getRequest() );                     // (3) ask servlet to update the data
    String[] errors = servlet.getValidationErrors();                       // (4) 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:

  1. Rather than simply asking for the response, we create a context in which we can step through the invocation of the servlet. This uses the same mechanisms to locate and initialize the servlet, request, and response objects, but does not actually invoke the servlet to process the request.
  2. We retrieve the initialized servlet and cast it in order to get at its intermediate methods.
  3. We call the updateBettingPool method (which we need to make package-accessible), passing the request object found in the context.
  4. We can now call a new method in the servlet which will return an array of error messages, which we can compare against our expected values for them.

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;
}

Displaying the error messages

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" );
    WebRequest request = form.getRequest( "save", "Open Pool" );               // (1) select a submit button

    request.setParameter( "away1", "Detroit Lions" );                          // (2) enter bad values into the form
    request.setParameter( "home1", "Denver Broncos" );
    request.setParameter( "home2", "Baltimore Ravens" );
    request.setParameter( "tiebreaker", "3" );
    response = client.getResponse( request );                                  // (3) submit the form

    WebTable errorTable = response.getTableStartingWithPrefix( "Cannot ope" ); // (4) Look for error table
    assertNotNull( "No errors reported", errorTable );
    String[][] cells = errorTable.asText();                                    // (5) 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:

  1. We select the "Open Pool" button to be included with the form submission.
  2. We then enter known bad values.
  3. We want the response when we submit the form changes.
  4. We expect to find them in a table which we can recognize because its upper-leftmost non-empty cell which starts with a known string.
  5. Since we want to examine the textual content of any non-empty cells in the table, we ask that the table be converted to a two-dimensional string array. In this case, there should only be one non-blank cell in each row.

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 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'>&nbsp;</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.

Closing the pool

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" );
    WebRequest request = form.getRequest( "save", "Open Pool" );

    request.setParameter( "away1", "Detroit Lions" );
    request.setParameter( "home1", "Denver Broncos" );
    request.setParameter( "away3", "Indianapolis Colts" );
    request.setParameter( "home3", "Baltimore Ravens" );
    request.setParameter( "tiebreaker", "3" );
    client.getResponse( request );                                                // (1) ignore the response

    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 {
        request = form.getRequest();
        request.setParameter( "home3", "Philadelphia Eagles" );                   // (4) try to change an entry
        fail( "Could still edit the pool" );
    } catch (IllegalRequestParameterException e) {}
}

Note:

  1. We are not interested in the response this time because we may ultimately have the browser forwarded to the main page.
  2. We come back to the form as though we were planning on editing it anew.
  3. We want to ensure that the submit buttons are disabled so the user cannot submit the form.
  4. We also verify that the fields are now marked readonly, which would prevent us from changing them. If the exception is not thrown, the test will be marked as failing.

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.