Sometimes debugging an automated test case is problematic, specifically when the test case has many stages built into it. Creating isolated stages can lead to a better identification of the issue. To address this problem we can use a design pattern called State Machine.
What is state machine pattern?
A state machine is an environment that manages the data stored about an object in a specific part of the execution. Each state is selected based on the given input from a previous state and dictates the outcome based on the logic it performs, sending the output to the next state.
Defining our generic test steps
In this part, we define our TestCase machine class. This will keep a record of all the states but does not know the specific order in which to execute. That information is stored inside each state class.
Let us take a basic test scenario where we want to execute logic before the test case, then run the test, perform some operations after, and in the end report the results. For this, out test machine class will look like this:
class TestMachine(object): before = None run = None after = None report = None @staticmethod def run_all(): current_state = TestMachine.before current_state.run() while current_state.next() is not None: current_state = current_state.next() current_state.run()
Notice here that we used static definitions for our TestMachine class. This means that class can, and must, be reused each time when executing or defining a new test case.
Creating the specific State classes
Since we have defined four states in out TestMachine class, it falls in order to define 4 states.
The states have been designed in order to be reused with different logic, which is injected in their run methods. The more specific detail is the pointer to the next state in the execution process.
class RunTestState(State): def __init__(self, run_def): super().__init__() self._run = run_def def run(self): self._run() def next(self): return TestMachine.after class BeforeState(State): def __init__(self, run_def): ... def next(self): return TestMachine.run class AfterState(State): def __init__(self, run_def): ... def next(self): return TestMachine.report class ReportState(State): def __init__(self, run_def): ... def next(self): return None
In this way, we have defined the link between the states: the execution starts with the BeforeState, which jumps from state to state using the next() method until it gets to the ReportState which has no successor.
Injecting test logic for each state
Now that we have modeled the behavior of our state system, the next step is to set the logic of our states. The way the states have been defined allows for functions to be passed as parameters in order to fully customize the execution logic. As a consequence, the states can be reused by just changing the function with is injected into them.
For the sake of exemplifying we will inject functions with simple logic, like a print statement.
def before_def(): print("before") def run_def(): print("running") def after_def(): print("after") def report_def(): print("report")
How all comes together
The final step is to link this all together. For this, we will use the TestMachine’s static attributes in order to set the corresponding value for a test:
TestMachine.before = BeforeState(before_def) TestMachine.run = RunTestState(run_def) TestMachine.after = AfterState(after_def) TestMachine.report = ReportState(report_def) TestMachine.run_all()
The method definitions and the test run is specific for a single test, in order to prepare for a new test we have to change the methods that hold the logic. This leads to a better test step modularization and helps to define specific methods that perform specific operations.