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" );

    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:

  1. Rather than simply submitting the form, we ask for the request object used to do the submission. This was hidden from us in the last test.
  2. Given the request, 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.
  3. We retrieve the initialized servlet and cast it in order to get at its intermediate methods.
  4. We call the updateBettingPool method (which we need to make package-accessible), passing the request object found in the context.
  5. 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" );

    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:

  1. We enter known bad values.
  2. We then select the "Open Pool" button to be included with the form submission.
  3. We want the response when we submit the form changes.
  4. We expect to find them in a table with "errors" as its ID.
  5. We are not interested in any empty cells used for formatting.
  6. 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 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'>&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" );

    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:

  1. Since we are not interested in the response this time (because we may ultimately have the browser forwarded to the main page), we simply click the submit button. This does the same as calling form.submit(), but does not return a response value.
  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.