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:
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(); } }
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.
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:
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.
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.