[ Team LiB ] Previous Section Next Section

6.2 Event Listener Testing

6.2.1 Problem

You want to create a mock implementation of an event listener interface.

6.2.2 Solution

Write a class that implements the interface, but only define behavior for the methods you need for your current test.

6.2.3 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.

6.2.4 See Also

The next recipe shows how to simplify the tests by using a mock object that encapsulates the validation logic.

    [ Team LiB ] Previous Section Next Section