Delve into the Rich Test Model

29 04 2010

The previous post introduced the concept of the Rich Test Model. This article shows the source code of the model and how it can be enriched with further behavior in order to support more test scenarios.

The TransferTestModel source code looks like this:

public class TransferTestModel {
  private Account debitAccount;
  private Account creditAccount;
  private Date transferDate;
  private Money transferAmount;

  private TransferTestModel() {} // not instantiable

  public static TransferTestModel startTransferFromPrivateAccount() {
    return new TransferTestModel() {
      {
        debitAccount = new PrivateAccount(
          "1122334455", new Money("CHF", 5000));
      }
    }
  }

  public TransferTestModel toForeignBankAccount() {
    creditAccount = new GenericBankAccount(
      "9988776655", new Address("Money Receiver", "1234", "Somewhere"));
    return this;
  }

  public TransferTestModel asSoonAsPossible() {
    transferDate = DateUtils.nextBusinessDay();
    return this;
  }

  public TransferTestModel amountGreaterThanAvailableMoney() {
    transferAmount = debitAccount.getBalance().add(1);
    return this;
  }

  public Transfer build() {
    return new Transfer(
      creditAccount, debitAccount, transferDate, transferAmount);
  }
}

This class follows the Builder pattern and consists of three parts:

  1. The static (factory) method startTransferFromPrivateAccount starts the build process. (mandatory data)
  2. The three member methods toForeignBankAccount, asSoonAsPossible, and amountGreaterThanAvailableMoney define the transfer details. (optional data)
  3. The build method finishes the build process and returns the Transfer object.

Even though the TransferTestModel has only one static and three member methods, it already supports eight different test scenarios:

= <number of static methods> * (2 ^ <number of member methods>)

By adding

  • new static factory methods (e.g. for account types applying different business rules) and
  • new member methods (e.g. for further business aspects, threshold testing, or special cases which need complex initializations),

the model will be enriched and more test scenarios are possible.

Assume we want to test transfers from a savings account (with a different “available money” policy), transfers scheduled on a weekend day, and transfers scheduled in the past. Let’s see how the model will be expanded…

public class TransferTestModel {
  public static TransferTestModel startTransferFromSavingsAccount() {
    return new TransferTestModel() {
      {
        debitAccount = new SavingsAccount(
          "12356789", new Money("CHF", 150000));
      }

      @Override
      public TransferTestModel amountGreaterThanAvailableMoney() {
        // a maximum of CHF 50'000 can be transferred
        // from a savings account
        transferAmount = new Money("CHF", 50001);
        return this;
      }
    }
  }

  public TransferTestModel onWeekendDay() {
    Calendar cal = Calendar.getInstance();
    int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
    int offset = Math.min(1, Calendar.SATURDAY - dayOfWeek);
    cal.add(Calendar.DAY_OF_MONTH, offset);
    transferDate = cal.getTime();
    return this;
  }

  public TransferTestModel inThePast() {
    Calendar cal = Calendar.getInstance();
    cal.add(Calendar.DAY_OF_MONTH, -1);
    transferDate = cal.getTime();
    return this;
  }

  // snipped code of first sample (see above)
}

The startTransferFromSavingsAccount method initializes a transfer using a savings account and overrides the default amountGreaterThanAvailableMoney method. Of course, a real-life test model would not use hardcoded amounts and policy rules (as used in the example above) but would rather delegate this to the business layer.

By overriding the default implementation, the static factory method defines and therefore encapsulates the (context bound) test behavior.  A unit test based on this new method does not care about these details:

public class TransferServiceTest {
  @Test
  public void testTransferUsingInvalidAmount() {
    Transfer transfer = TransferTestModel
      .startTransferFromSavingsAccount()
      .toForeignBankAccount()
      .asSoonAsPossible()
      .amountGreaterThanAvailableMoney()
      .build();

    Transaction transaction = transferService.transfer(transfer);
    // Assert expected transaction details
  }
}

This test only denotes that it wants to transfer an “invalid” amount but does not know what “invalid” means in terms of a savings account – this knowledge is hidden in the test model.

Outlook

A further article will focus on test assertions and method naming… stay tuned!

Advertisement

Actions

Information

One response

29 04 2010
Mischa

Very nice idea! “Testing Is Fun” will slowly come true… I’m curious to read more on this!

Cheers Mischa

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s




Follow

Get every new post delivered to your Inbox.