Implementing State-based Testing in Smalltalk

Implementing State-based Testing in Smalltalk

Authors:


Introduction

State-based testing is a diagnostic technique for OO design based on state models. In state models, objects are characterized by states. States are described by discrete state variables. The value of state variables depend on the instance variables that contribute to the state. The mapping from instance variables to state variables can be expressed in terms of a logical expression. For example, suppose we have a class of object called Water with the following instance variable, temperature, and three states, ice, water, and steam, the following define the mapping from instance variable to state variable

To perform state-based testing for a class of objects, for each method in the class, we specify the possible states that the object can be in prior to the message send, known as the pre-condition. We also specify the possible states that the object can be in after the message send, known as the post-condition. A sequence of messages are sent to the object to test all the possibilities specified in the pre and post-conditions. The states prior to and after each message send are retained so that we can check to see that each method altered the states according to the pre-specified conditions.

Smalltalk Implementation

To implement state-based checking for a particular class in Smalltalk, we start by defining binary states that are mutually exclusive and collectively exhaustive. To store state definitions, we create a class variable called StateDictionary, an instance of Dictionary. The keys of the dictionary are state names; the values are blocks containing logical expressions that map instance variables to state names. When the expression evaluates to true, the corresponding state is considered to be on; when the expression evaluates to false, the corresponding state is considered to be off. Using our Water example, the state dictionary would be as follows

To implement facilities for defining pre- and post-conditions for methods, we introduce another class variable called PreAndPostConditions, an instance of Dictionary, to store the conditions. The keys are method names; the values are arrays consisting of two blocks, the pre-condition block and the post-condition block. States must be passed into each block as arguments. The pre-condition block needs to know the state before the execution of a method and the post-condition block needs to know both the states before and after the execution of a method. Each block evaluates to true when conditions are met and false when conditions are not met.

Using the Water class example, the PreAndPostConditions is as follows

Key: #cool:  Value: #([:before | true]
   [:before :after | 
   ((before = #ice) & (after = #ice))| 
   ((before = #water) & ((after = #water) |
    (after = #ice)) |
   ((before = #steam) & ((after = #steam) |
    (after = #water)| (after = #ice))])
 
Key:  #sublime    Value: #([:before | before = #ice] 
    [:before :after | after = #steam])

Key: #insulate    Value: #([:before | true] 
    [:before :after | before = after])

Having defined the two storage dictionaries, we modify the method dictionary of the class. First, for each method being tested, we concatenate 'STATECHECKED' to the front of the method name. Using our example, we define the following methods for Water

These methods would be renamed to be

Second, we use the old method names to define new methods with hooks. Using the sublime method as an example, the new method with ancillary methods is as follows

sublime
 |temp stateBefore stateAfter|
 stateBefore := self stateCheck.
 Transcript show: 'State before method execution ', stateBefore; cr;
 Transcript show: 'Pre-Condition Status:', 
  (self preConditionCheck: stateBefore forMethod:
     #sublime) asString.
 temp := self STATECHECKEDsublime.
 stateAfter := self stateCheck.
 Transcript show: 'State after method execution ', stateAfter; cr.
 Transcript show: 'Post-Condition Status: ',
  (self postConditionCheck: stateBefore and: stateAfter
   forMethod: #sublime) asString.
 ^temp.

stateCheck
 StateDictionary
  associationsDo: [:a | (a value value: self)
   ifTrue: [ ^a key]].

preConditionCheck: stateBefore forMethod: methodName
 ^((PreAndPostConditions at: methodName) at: 1) value: stateBefore.

postConditionCheck: stateBefore and: stateAfter forMethod: methodName
 ^((PreAndPostConditions at: methodName) at: 2) value:
  stateBefore value: stateAfter.

Remarks

For clarity of presentation, we have greatly simplified the implementation details. In practice, a separate class that handles state testing would be created to minimize invasiveness. The class would have both the state dictionary and the conditions dictionary as class variables. The state dictionary would first be organized by class, then by state variable names, then by state variable values and corresponding mapping conditions. The test condition dictionary would first be organized by class, then by method and corresponding pre and post conditions.

Our state-based testing implementation draws ideas from an article by Robert V. Binder entitled "State-Based Testing" published in July-August issue of Object Magazine. The idea of altering method dictionaries to query states was inspired by the use of tracers as a testing tool from a paper by Heinz-Dieter Bocker and Jurgen Herczeg entitled "What Tracers Are Made of" found in the ECOOP/OOPSLA '90 Proceedings.