State-based testing techniques

One commonly used testing technique is to analyze the different abstract states that a class can take. The state of an object is generally defined as a constraint on the values of its attributes. According to the state of the object, calls to certain methods may or may not be valid, or the method's behavior may change.

Generally speaking, the process of using state-based testing techniques is as follows:

  1. Define the steps.
  2. Define the transitions between states.
  3. Define test scenarios.
  4. Define test values for each state.

To see how this works, consider the MoneyBag class from JUnit.

class MoneyBag implements IMoney {
     private Vector fMonies= new Vector(5); 
     public IMoney add(IMoney m) {
         return m.addMoneyBag(this);
     }
     public IMoney addMoney(Money m) {
         return MoneyBag.create(m, this);
  } 
  public IMoney addMoneyBag(MoneyBag s) {
     return MoneyBag.create(s, this); 
  }
  void appendMoney(Money aMoney) {
     if (aMoney.isZero()) return;
     IMoney old= findMoney(aMoney.currency());
     if (old == null) {
           fMonies.addElement(aMoney);
           return;
     }
     fMonies.removeElement(old);
     IMoney sum= old.add(aMoney);
     if (sum.isZero())
           return;
     fMonies.addElement(sum);
  }
  private Money findMoney(String currency) {
     for (Enumeration e=fMonies.elements();e.hasMoreElements();)
     {
          Money m= (Money) e.nextElement();
          if (m.currency().equals(currency))
              return m;
     }
     return null;
  }
  private boolean contains(Money m) {
     Money found= findMoney(m.currency());
     if (found == null) return false;
     return found.amount() == m.amount();
     }
  } 

Define states

The first step in using state-based testing techniques is to define the states. From the second line in the code, you can see that the MoneyBag class can hold from 0 to 5 Money objects.

private Vector fMonies= new Vector(5); 

From this analysis, you can create a state model with the following states:

In this example, you can define the following constraints on the fMonies attribute, as shown in the following table:

State Constraint
EmptyBag fMonies.size()==0
PartiallyFullBag (fMonies.size()>0) && (fMonies.size()<5)
FullBag fMonies.size()==5

Although it is not always necessary to formally define these states, it can be useful when you are defining test data, or if you want to check the object state during a specific scenario.

Define transitions between states

The next step is to define the possible transitions between states and determine what triggers a transition from one state to another. Generally, when you test a class, a transition is triggered when a method is invoked. For example, the transition from the EmptyBag state to the PartiallyFullBag state is triggered by a call to appendMoney.

Thus, some possible transitions could be defined as follows:

To summarize, for each identified state, you should list:

Define test scenarios

Tests generally consist of scenarios that exercise the object along a given path through the state machine. Since the number of possible paths in the state machine is generally infinite, it is not practical to test each possible path. Instead, you should make sure that you do the following tasks:

Whenever possible, check the state of the object that you are testing throughout the scenario to ensure that the theoretical state model you have defined is actually the one implemented by the class you are testing. After you finish with these transitions, you can test for robustness by calling methods in a random sequence and checking that a class invariant is never violated. For instance, the MoneyBag class should always be a set of Money objects that are never of the same currency.

You can use the scenario-based test pattern that is included with the product to create test scenarios.

Define test values for each state

Finally, you need to choose test values for each individual state. Choose unique test values and do not reuse values that you have used previously in the context of other tests. This strategy provides more diversity in your test suite and increases the likelihood of detecting bugs. For details about defining appropriate test values, see Data partitioning techniques.

Related concepts
Test strategies
Java subsystems

Related tasks
Testing Java methods
Testing Java classes

Terms of use | Feedback
(C) Copyright IBM Corporation 2000, 2005. All Rights Reserved.