previous post in category
One of the advantages of using object state machines to describe object behavior in the MBSE approach is that they are very easy to test at the unit test level. All one has to do is to disable the event queue manager so that it only executes the first event placed in the event queue and just invokes a logging facility in the test harness to log any event placed on the queue subsequently by the action under test. Then to test a state action, one just pushes the relevant transition event onto the event queue.
This is easy because a state action is completely defined by three things: (A) the state of the application prior to executing the action, (B) the state of the application after executing the action, and (C) any messages the action generates (i.e., the events it places on the event queue). The state of the application is represented by the values of the state variables (object attributes). In a well-formed OO application a method accesses any data it needs directly so one knows exactly what attributes need to be initialized prior to execution by simply inspecting the method. The test case specification will indicate exactly what state variables will be modified when the action executes.
Because we construct the behaviors in MBSE using an asynchronous collaboration model, we don't care what might happen in response to any events the action under test places on the event queue. This is important because it means that we can specify an action's interaction with other objects fully in terms of the collaboration messages it creates rather than hierarchically through the specification of the resulting behaviors. [Remember that in OOA/D collaboration messages are announcements of something the sender did rather than imperatives about what the receiver should do. So the specification of the behavior responsibility is limited to what the behavior does and what it should announce about what it did.]
These things all conspire so that all we need to instantiate in the application, besides the action under test, are any objects containing attributes read or written by the action under test. These objects are effectively stubs because we will not execute any of their behaviors. This makes life very easy for the test harness because all we need is the Class Diagram definition for these objects and it is fairly easy to automate both the attribute initialization and validation of the attributes after execution. Thus the basic unit test process is:
(1) Instantiate the application objects. This essentially means creating an instance of the object under test and a stub instance for any objects that have attributes accessed by the methods of the object under test.
(2) Initialize the application objects. Initialize any attributes that the object under test accesses to appropriate values. For attributes that the object reads, choose values consistent with the specified initial conditions for the test case. For attributes that the object writes, choose values that are different than those specified as outputs by the test case.
(3) Initialize the current state of the object under test. This should be to a state that is appropriate to the transition associated with the test event. (The test should specify the state the state machine is expected to be in for a specific unit test case.)
(4) Push an event onto the event queue. This event is specified in the test case to invoke a particular state action. If the event queue implementation requires that the queue be formally started, do so to execute the event.
(5) Validate the application state. Examine the state of the application. Essentially this means validating that any attributes the action writes have the value specified in the test case.
(6) Validate the collaborations. Examine any events generated by the action to ensure the correct event ID, object address, and data packet were supplied as specified in the test case.
Caveat: this scheme only does positive testing (i.e., it validates that the action does what its responsibility was specified to do). It does not demonstrate that the action does not do something it shouldn't (i.e., that it does not update an attribute that it is not supposed to update). One can do negative testing but it entails instantiating every possible object that might be created during the execution, initializing all their attributes to some flag value, and then validating that the flag values did not change when the action under test executes. Then one must repeat the same test with a different flag value (in case the action under test coincidentally writes exactly the flag value). [Even this is not fool-proof since the action under test may simply access one attribute it shouldn't and then write that value someplace else it shouldn't. Negative testing is always difficult but this unit testing approach covers most of the bases.] Whether such negative testing is feasible is an economic issue for the development environment, but this approach gets one close to exhaustive negative testing when it is justified.
This process represents pretty vanilla test execution, so it should be fairly easy to automate from a test harness. For elaborated code there is some trickiness involved in setting the current state of the state machine since typically the current state is a private attribute with no public interface. In a language like C++ one can make the test harness a "friend" and provide private access that way. Otherwise one needs to provide a public setter for it that only the test harness accesses. (Since public vs. private at the 3GL level is irrelevant in translation-based development, one typically provides a setter as a matter of course.)
It is important to note that this ease of use is enabled by good OO practices that are religiously followed. That's because the rules of state machines enforce good OO practices, such as self-contained methods. One can do the same thing without state machines but it requires substantial discipline that is often lacking in OOP. For example, all behaviors must be procedures rather than functions so that they can be fully stubbed without affecting the specification of the object under test. In addition, any called methods cannot modify attributes that the caller accesses after the call.