Cooking with Java XP, Part 2
|
Related Reading
Java Extreme Programming Cookbook |
Editor's note: The first recipe we plucked from the pages of Java Extreme Programming Cookbook showed you how to set up an efficient development environment using an Ant buildfile. In these two recipes, excerpted from Chapter 6 on "Mock Objects," learn how to create a mock implementation of an event listener interface, and how to avoid duplicated validation logic in your tests. And check back here next week for recipes on creating a load test and executing a custom template.
Event Listener Testing
Problem
You want to create a mock implementation of an event listener interface.
Solution
Write a class that implements the interface, but only define behavior for the methods you need for your current test.
Discussion
Java Swing user interfaces rely heavily on models and views. In the case of
tables, for instance, the TableModel interface is the model and JTable is one
possible view. The table model communicates with its view(s) by sending
TableModelEvents whenever its data changes. Since numerous views may
observe a single model, it is imperative that the model only sends the minimum
number of events. Poorly written models commonly send too many events, often
causing severe performance problems.
Let's look at how we can use mock objects to test the events fired by a
custom table model. Our table model displays a collection of Account
objects. A mock table model listener verifies that the correct event is
delivered whenever a new account is added to the model. We'll start with the
Account class, as shown in Example 6-1.
Example 6-1. The Account class
package com.oreilly.mock;
public class Account {
public static final int CHECKING = 0;
public static final int SAVINGS = 1;
private int acctType;
private String acctNumber;
private double balance;
public Account(int acctType, String acctNumber, double balance) {
this.acctType = acctType;
this.acctNumber = acctNumber;
this.balance = balance;
}
public int getAccountType( ) {
return this.acctType;
}
public String getAccountNumber( ) {
return this.acctNumber;
}
public double getBalance( ) {
return this.balance;
}
}
Our table model consists of three columns of data, for the account type, number, and balance. Each row in the table represents a different account. With this knowledge, we can write a basic table model as shown next in Example 6-2.
Example 6-2. Account table model
package com.oreilly.mock;
import javax.swing.table.AbstractTableModel;
import java.util.ArrayList;
import java.util.List;
public class AccountTableModel extends AbstractTableModel {
public static final int ACCT_TYPE_COL = 0;
public static final int ACCT_BALANCE_COL = 1;
public static final int ACCT_NUMBER_COL = 2;
private List accounts = new ArrayList( );
public int getRowCount( ) {
return this.accounts.size( );
}
public int getColumnCount( ) {
return 3;
}
public Object getValueAt(int rowIndex, int columnIndex) {
Account acct = (Account) this.accounts.get(rowIndex);
switch (columnIndex) {
case ACCT_BALANCE_COL:
return new Double(acct.getBalance( ));
case ACCT_NUMBER_COL:
return acct.getAccountNumber( );
case ACCT_TYPE_COL:
return new Integer(acct.getAccountType( ));
}
throw new IllegalArgumentException("Illegal column: "
+ columnIndex);
}
public void addAccount(Account acct) {
// @todo - implement this!
}
}
Tests for the getRowCount( ), getColumnCount( ), and
getValueAt( ) methods are not shown here. To test these methods, you
can create an instance of the table model and call the methods, checking for the
expected values. The addAccount( ) method, however, is more interesting
because it requires a mock object.
The mock object is necessary because we want to verify that calling
addAccount( ) fires a single TableModelEvent. The mock object
implements the TableModelListener interface and keeps track of the
events it receives. Example 6-3 shows such a mock object. This is a primitive
mock object because it does not provide a way to set up expectations, nor does
it provide a verify( ) method. We will see how to incorporate these
concepts in coming recipes.
Example 6-3. Mock table model listener
package com.oreilly.mock;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import java.util.ArrayList;
import java.util.List;
public class MockTableModelListener implements TableModelListener {
private List events = new ArrayList( );
public void tableChanged(TableModelEvent e) {
this.events.add(e);
}
public int getEventCount( ) {
return this.events.size( );
}
public List getEvents( ) {
return this.events;
}
}
The mock object implements TableModelListener and keeps a list of
all events received. The unit test creates an instance of the mock object, adds
it as a listener to the custom table model, and calls the addAccount( )
method. Afterwards, it asks the mock object for the event list and verifies that
the correct event was delivered. The unit test is shown in Example 6-4.
Example 6-4. Account table model test case
package com.oreilly.mock;
import junit.framework.TestCase;
import javax.swing.event.TableModelEvent;
public class UnitTestAccount extends TestCase {
private AccountTableModel acctTableModel;
private Account[] accounts = new Account[]{
new Account(Account.CHECKING, "001", 0.0),
new Account(Account.CHECKING, "002", 1.1),
new Account(Account.SAVINGS, "003", 2.2)
};
protected void setUp( ) throws Exception {
this.acctTableModel = new AccountTableModel( );
for (int i = 0; i < this.accounts.length; i++) {
this.acctTableModel.addAccount(this.accounts[i]);
}
}
public void testAddAccountFiresCorrectEvent( ) {
// create the mock listener
MockTableModelListener mockListener =
new MockTableModelListener( );
// add the listener to the table model
this.acctTableModel.addTableModelListener(mockListener);
// call a method that is supposed to fire a TableModelEvent
this.acctTableModel.addAccount(new Account(
Account.CHECKING, "12345", 100.50));
// verify that the correct event was fired
assertEquals("Event count", 1, mockListener.getEventCount( ));
TableModelEvent evt = (TableModelEvent)
mockListener.getEvents( ).get(0);
assertEquals("Event type",
TableModelEvent.INSERT, evt.getType( ));
assertEquals("Column",
TableModelEvent.ALL_COLUMNS, evt.getColumn( ));
assertEquals("First row",
this.acctTableModel.getRowCount( )-1,
evt.getFirstRow( ));
assertEquals("Last row",
this.acctTableModel.getRowCount( )-1,
evt.getLastRow( ));
}
}
With the test in hand (and failing), we can implement the addAccount(
) method as shown here:
public void addAccount(Account acct) {
int row = this.accounts.size( );
this.accounts.add(acct);
fireTableRowsInserted(row, row);
}
The method takes advantage of the fact that our table model extends from
AbstractTableModel, which provides the fireTableRowsInserted(
) method. The unit test verifies that addAccount( ) calls this
method rather than something like fireTableDataChanged( ) or
fireTableStructureChanged( ), both common mistakes when creating custom
table models. After writing the method, the test passes.
You can follow this technique as you add more functionality to the custom table model. You might add methods to remove accounts, modify accounts, and move rows around. Each of these operations should fire a specific, fine-grained table model event, which subsequent tests confirm using the mock table model listener.
The next recipe shows how to simplify the tests by using a mock object that encapsulates the validation logic.
Pages: 1, 2 |