Rich Test Model

26 03 2010

Introduction

How do you manage complex test scenarios especially the variety of parameters, entities, and their interdependencies? What are your experiences maintaining these test cases?

According to my experience, writing automated tests does not implicitly mean that these tests are useful and maintainable. Furthermore, writing tests for a complex domain can become a pretty tricky task. A lot of test methods are full of cumbersome (and itself error-prone) initialization code to build the test parameters and to invoke the methods under test.

This article shows the drawbacks of a hard coded, naïve test approach and proposes a “rich test model” to address those issues.

Test at first feel

Let’s transfer some money and test the transfer service:

public class TransferServiceTest {
  @Test
  public void testTransferWithInvalidAmount() {
    Transfer transfer = new Transfer();
    transfer.setDebitAccount("1122334455");
    transfer.setCreditAccount("9988776655",
      new Address("Money Receiver", "1234", "Somewhere"));
    transfer.setTransferDate(new Date());
    transfer.setAmount("EUR", 1000);

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

There are some issues with this approach:

  1. Explicit interdependencies.
    The explicit usage of account numbers, address data, date, and amount requires detailed knowledge about the data structures and their relationships. Replacing the hard coded values by constants would not solve the problem as we still need to know which values suit together for this test scenario.
  2. Complicated threshold value testing.
    Since the transfer amount and date are explicitly set, threshold value testing becomes cumbersome. Assume you want to test the following scenarios: the transfer amount is 1) lower, 2) equal, and 3) greater than the currently available money. You will need to know the type of account “1122334455″, the current balance and the allowed overdraw policy of this account. This is too much information for such a simple example.
  3. Violated DRY principle.
    If different test scenarios are based on the same test data set, we run the risk of violating the DRY (don’t repeat yourself) principle because the “knowledge” of these data interdependencies is spread all over the tests.
  4. Maintenance hell.
    Automated tests are an essential key factor for successful projects. At first feel, automated tests seem to behave nicely. But if you need to change those tests – and this happens – they can show their true colors. What happens if interdependencies change, or if new business rules affect all your tests? In the worst case you have to change hundreds of test scenarios.

This rule addresses the aforementioned issues:

Test methods must express what they test, not how they test.

Separate test method from the model

Let’s change the above code snippet a little bit:

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

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

This code is the result of a strict separation of concerns (SoC). The test method just describes what it wants to test: a transfer from a private account to a foreign bank account, transfer the money as soon as possible, and use an amount which is greater than the available money.

All the transfer details (account numbers, addresses, exact date and amount) are abstracted and hidden in the TransferTestModel class. The TransferTestModel class follows the builder pattern and knows the domain internals and how to parameterize the test data. This builder class represents a test model with rich behavior. I therefore call it a “rich test model“.

Conclusions

Effective tests focus on what to test. Technical details and data interdependencies should be abstracted and hidden in a rich test model based on the domain under test. The resulting rich test model builds a (test-driven) DSL and is therefore an ideal discussion basis for developers, testers, and domain experts.

The separation of concerns (rich test model and test scenario) allows to successfully manage data interdependencies and to minimize the maintenance effort.

Outlook

In future articles I will show the details of the TransferTestModel class, enrich the model in order to support more sophisticated tests, and emphasize the importance of the ubiquitous language in the rich test model.

Advertisement

Actions

Information

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.