Developer Forums | About Us | Site Map
Search  
HOME > TUTORIALS > SERVER SIDE CODING > JAVA TUTORIALS > UNIT TESTING WITH MOCK OBJECTS


Sponsors





Useful Lists

Web Host
site hosted by netplex

Online Manuals

Unit testing with mock objects
By Alexander Day Chaffee & William Pietri - 2004-02-18 Page:  1 2 3 4 5 6

Mechanics

A refactoring consists of many small, technical steps. Together, these are called the mechanics. If you follow the mechanics closely like a cookbook recipe, you should be able to learn the refactoring without much trouble.

  1. Identify all occurrences of code that create or obtain the collaborator.

  2. Apply the Extract Method refactoring to this creation code, creating the factory method (discussed on page 110 of Fowler's book; see the Resources section for more information).

  3. Assure that the factory method is accessible to the target object and its subclasses. (in the Java language, use the protected keyword).

  4. In your test code, create a mock object implementing the same interface as the collaborator.

  5. In your test code, create a specialization object that extends (specializes) the target.

  6. In the specialization object, override the creation method to return a mock object that accommodates your test.

  7. Optional: create a unit test to assure that the original target object's factory method still returns the correct, non-mock object.

Example: ATM

Imagine you are writing the tests for a bank's Automatic Teller Machine. One of those tests might look like Listing 2:

Listing 2. Initial unit test, before mock object introduction


  public void testCheckingWithdrawal() {
    float startingBalance = balanceForTestCheckingAccount();

    AtmGui atm = new AtmGui();
    insertCardAndInputPin(atm);

    atm.pressButton("Withdraw");
    atm.pressButton("Checking");
    atm.pressButtons("1", "0", "0", "0", "0");
    assertContains("$100.00", atm.getDisplayContents());
    atm.pressButton("Continue");

    assertEquals(startingBalance - 100, 
balanceForTestCheckingAccount());
  }

In addition, the matching code inside the AtmGui class might look like Listing 3:

Listing 3. Production code, before refactoring


  private Status doWithdrawal(Account account, float amount) {
    Transaction transaction = new Transaction();
    transaction.setSourceAccount(account);
    transaction.setDestAccount(myCashAccount());
    transaction.setAmount(amount);
    transaction.process();
    if (transaction.successful()) {
      dispense(amount);
    }
    return transaction.getStatus();
  }

This approach will work, but it has an unfortunate side effect: the checking account balance is lower than when the test started, making other testing more difficult. There are ways to solve that, but they all increase the complexity of the tests. Worse, this approach also requires three round trips to the system in charge of the money.

To fix this problem, the first step is to refactor AtmGui to allow us to substitute a mock transaction for the real transaction, as shown in Listing 4 (compare the boldface source code to see what we're changing):

Listing 4. Refactoring AtmGui


  private Status doWithdrawal(Account account, float amount) {
    Transaction transaction = createTransaction();
    transaction.setSourceAccount(account);
    transaction.setDestAccount(myCashAccount());
    transaction.setAmount(amount);
    transaction.process();
    if (transaction.successful()) {
      dispense(amount);
    }
    return transaction.getStatus();
  }
  
  protected Transaction createTransaction() {
    return new Transaction();
  }

Back inside the test class, we define the MockTransaction class as a member class, as shown in Listing 5:

Listing 5. Defining MockTransaction as a member class


  private MockTransaction extends Transaction {

    private boolean processCalled = false;

    // override process method so that no real work is done
    public void process() {
      processCalled = true;
      setStatus(Status.SUCCESS);
    }

    public void validate() {
      assertTrue(processCalled);
    }
  }

And finally, we can rewrite our test so that the tested object uses the MockTransaction class rather than the real one, as shown in Listing 6.

Listing 6. Using the MockTransaction class


  MockTransaction mockTransaction;

  public void testCheckingWithdrawal() {

    mockTransaction = new MockTransaction();

    AtmGui atm = new AtmGui() {
        protected Transaction createTransaction() {
          return mockTransaction;
        }
    };

    insertCardAndInputPin(atm);

    atm.pressButton("Withdraw");
    atm.pressButton("Checking");
    atm.pressButtons("1", "0", "0", "0", "0");
    assertContains("$100.00", atm.getDisplayContents());
    atm.pressButton("Continue");

    assertEquals(100.00, mockTransaction.getAmount());
    assertEquals(TEST_CHECKING_ACCOUNT, 
mockTransaction.getSourceAccount());
    assertEquals(TEST_CASH_ACCOUNT, 
mockTransaction.getDestAccount());
    mockTransaction.validate();

}

This solution yields a test that is slightly longer, but is only concerned with the immediate behavior of the class being tested, rather than the behavior of the entire system that lies beyond the ATM's interface. That is, we no longer check that the final balance of the test account is correct; we would check that function in the unit test for the Transaction object, not the AtmGui object.

Note: According to its inventors, a mock object is supposed to perform all of its own validation inside its validate() method. In this example, for clarity, we left some of the validation inside the test method. As you grow more comfortable using mock objects, you will develop a feel for how much validation responsibility to delegate to the mock.



View Unit testing with mock objects Discussion

Page:  1 2 3 4 5 6 Next Page: Inner class magic

First published by IBM developerWorks


Copyright 2004-2024 GrindingGears.com. All rights reserved.
Article copyright and all rights retained by the author.