Zeta Workflow 08: Execution State Object
The execution state object retains the execution state as defined by isCancelled(), hasEnded(), and isSuspended(); and the execution variables as obtained via getVariables().
The functionality is:
- Capture execution state, given the execution as passed to the plugin.
- Disgorge the last-captured execution state. Mirror the execution methods isCancelled(), hasEnded(), isSuspended, getVariables(), getVariable(), hasVariable(), getId().
Our unit tests will explore the API of each method, followed by verifying the functionality in each case.
Starting Point
We begin with a list of unit test outcomes. I’m making this list off the top of my head based on the above description. We may add or change tests as we go:
- Can create a mock Zeta Workflow Execution object.
- The Zeta Workflow Execution object is an instance of the correct class, ezcWorkflowExecution.
- Can capture isCancelled().
- Can capture hasEnded().
- Can capture isSuspended().
- Can capture getVariables(). getVariable() and hasVariable are based on getVariables() content, so there is no need for separate capture mechanisms.
- Can capture getId().
- A new capture completely replaces any previous capture.
- captureExecution($execution) captures the above items 3-7.
- If any of the retrieval functions is called before the execution was captured, the retrieval function returns null.
- The retrieval functions return their respective captured isCancelled(), hasEnded(), isSuspended(), getVariables(), getId().
- getVariables(), when there are no variables, returns empty array.
- getVariable(), if the variable does not exist in the current capture, returns null.
- getVariable(), if the variable does exist in the current capture, returns the variable’s value.
- hasVariable() returns true or false for whether or not that variable exists in the capture.
We start coding by:
- Create code directory Sniffer and test directory tests/sniffer.
- Define a new test suite in the phpunit.xml.
- Define the test suite in the Makefile.
- Set up this test suite as the default target in the Makefile.
- Create a new test sniffer/executionStateTest.php which extends TestCase. We do not need to extend the database test case.
- Create a failing test with $this->fail(‘here’). Run the Makefile and observe the test failure.
- Proceed with the first of the above test outcomes.
Here is the initial unit test, set to fail:
<?php /** * Execution State Object * @package StarTribune/Workflow * * @author ewb (3/23/2014) */ namespace StarTribune\Workflow\Tests\Sniffer; use Mockery as m; /** * Execution State Object * @package StarTribune/Workflow */ class executionStateTest extends \TestCase { public function testHere() { $this->fail("here we are"); } }
With Makefile and phpunit.xml correctly set up, we see the failure:
vendor/bin/phpunit --testsuite single PHPUnit 4.0.12 by Sebastian Bergmann. Configuration read from ***/phpunit.xml F Time: 66 ms, Memory: 8.00Mb There was 1 failure: 1) StarTribune\Workflow\Tests\Sniffer\executionStateTest::testHere here we are ***/workbench/star-tribune/workflow/tests/sniffer/executionStateTest.php:17 FAILURES! Tests: 1, Assertions: 0, Failures: 1. make: *** [single] Error 1
Now that we know we are running the tests we think we are, we can proceed.
Mock Object
We start with a couple of tests ensuring we get our mock objects set up correctly.
In each case, we start by observing the test failing, then watch it pass. With the first test, we comment-out the assert to ensure that the expectation fails for not being met. In the second test, we just let it ride; it’s trivial. But it’s important to have the test because it “nails down” the execution class.
public function testMockSetup() { $this->execution->shouldReceive('myMethod') ->once() ->andReturn('foo'); $this->assertEquals('foo', $this->execution->myMethod()); } public function testMockCorrectClass() { $this->assertTrue($this->execution instanceof \ezcWorkflowExecution); }
The Capture Methods
We write the tests first. In this case we only confirm that the (not yet existent) functions are callable. When we get to the retrieval functions, we’ll flesh out the actual capture code.
The first test requires that we create the actual class.
public $execution; public $fixture; public function setUp() { $this->execution = m::mock('\ezcWorkflowExecution'); $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionState; } public function testFixtureCorrectClass() { $this->assertTrue($this->fixture instanceof \StarTribune\Workflow\Sniffer\ExecutionState); }
Once the test passes, we refactor the construction to setUp() as shown above. Here is our class.
<?php /** * Track Zeta Workflow execution state. * @package StarTribune/Workflow */ namespace StarTribune\Workflow\Sniffer; /** * Track Zeta Workflow execution state. * * @package StarTribune/Workflow * * @author ewb (3/29/2014) */ class ExecutionState { }
Now that the class exists, we write just enough code to make our capture tests pass.
The first pair of capture tests looks like this:
public function testCaptureIsCancelledAPI() { $this->execution->shouldReceive('isCancelled')->once(); $return = $this->fixture->captureIsCancelled($this->execution); $this->assertTrue(null === $return, "Method should return null"); } public function testCaptureIsCancelledCalls() { $this->execution->shouldReceive('isCancelled')->once(); $return = $this->fixture->captureIsCancelled($this->execution); }
Now that we’ve written it, we can see the first of the two tests is redundant. Any purpose served by the first test is served by the second.
Here is the class as written to pass the above tests.
class ExecutionState { public $isCancelled; public function captureIsCancelled(\ezcWorkflowExecution $execution) { $this->isCancelled = $execution->isCancelled(); } }
The rest of the captures and production code will be similar. Here are the tests.
public function testCaptureIsCancelledCalls() { $this->execution->shouldReceive('isCancelled')->once(); $return = $this->fixture->captureIsCancelled($this->execution); } public function testCaptureHasEnded() { $this->execution->shouldReceive('hasEnded')->once(); $return = $this->fixture->captureHasEnded($this->execution); } public function testCaptureIsSuspended() { $this->execution->shouldReceive('isSuspended')->once(); $return = $this->fixture->captureIsSuspended($this->execution); } public function testCaptureGetVariables() { $this->execution->shouldReceive('getVariables')->once(); $return = $this->fixture->captureGetVariables($this->execution); } public function testCaptureGetId() { $this->execution->shouldReceive('getId')->once(); $return = $this->fixture->captureGetId($this->execution); } public function testCaptureExecution() { $this->execution->shouldReceive('isCancelled')->once(); $this->execution->shouldReceive('hasEnded')->once(); $this->execution->shouldReceive('isSuspended')->once(); $this->execution->shouldReceive('getVariables')->once(); $this->execution->shouldReceive('getId')->once(); $return = $this->fixture->captureExecution($this->execution); }
Here is the production code.
class ExecutionState { public $isCancelled; public $hasEnded; public $isSuspended; public $variables; public $id; public function captureIsCancelled(\ezcWorkflowExecution $execution) { $this->isCancelled = $execution->isCancelled(); } public function captureHasEnded(\ezcWorkflowExecution $execution) { $this->hasEnded = $execution->hasEnded(); } public function captureIsSuspended(\ezcWorkflowExecution $execution) { $this->isSuspended = $execution->isSuspended(); } public function captureGetVariables(\ezcWorkflowExecution $execution) { $this->variables = $execution->getVariables(); } public function captureGetId(\ezcWorkflowExecution $execution) { $this->id = $execution->getId(); } public function captureExecution(\ezcWorkflowExecution $execution) { $this->captureIsCancelled($execution); $this->captureHasEnded($execution); $this->captureIsSuspended($execution); $this->captureGetVariables($execution); $this->captureGetId($execution); } }
Retrieval Functions
We are skipping the rule “a new capture completely replaces the old capture”. It will be clear from inspecting the production code that this is the case, and I don’t think the test is sufficiently worth the time to write it.
Let’s test that any retrieval prior to the first capture returns null.
public function testIsCancelledNull() { $actual = $this->fixture->isCancelled(); $this->assertTrue(null === $actual); } public function testHasEndedNull() { $actual = $this->fixture->hasEnded(); $this->assertTrue(null === $actual); } public function testIsSuspendedNull() { $actual = $this->fixture->isSuspended(); $this->assertTrue(null === $actual); } public function testGetVariablesNull() { $actual = $this->fixture->getVariables(); $this->assertTrue(null === $actual); } public function testGetIdNull() { $actual = $this->fixture->getId(); $this->assertTrue(null === $actual); }
I changed the captured properties from public to private. Here is the new production code.
class ExecutionState { private $_isCancelled; private $_hasEnded; private $_isSuspended; private $_variables; private $_id; public function captureIsCancelled(\ezcWorkflowExecution $execution) { $this->_isCancelled = $execution->isCancelled(); } public function captureHasEnded(\ezcWorkflowExecution $execution) { $this->_hasEnded = $execution->hasEnded(); } public function captureIsSuspended(\ezcWorkflowExecution $execution) { $this->_isSuspended = $execution->isSuspended(); } public function captureGetVariables(\ezcWorkflowExecution $execution) { $this->_variables = $execution->getVariables(); } public function captureGetId(\ezcWorkflowExecution $execution) { $this->_id = $execution->getId(); } public function captureExecution(\ezcWorkflowExecution $execution) { $this->captureIsCancelled($execution); $this->captureHasEnded($execution); $this->captureIsSuspended($execution); $this->captureGetVariables($execution); $this->captureGetId($execution); } public function isCancelled() { return $this->_isCancelled; } public function hasEnded() { return $this->_hasEnded; } public function isSuspended() { return $this->_isSuspended; } public function getVariables() { return $this->_variables; } public function getId() { return $this->_id; } }
To test that the retrieval really does retrieve the right information, we should go to a different test fixture setup. This means starting a new test class. Here is tests/sniffer/executionStateRetrievalTest.php, with the retrieval functions developed.
class executionStateRetrievalTest extends \TestCase { public $execution; public $fixture; public $expectedVariables = array('one' => 1, 'two' => 2); public $expectedId = 1234; public function setUp() { $this->execution = m::mock('\ezcWorkflowExecution'); $this->fixture = new \StarTribune\Workflow\Sniffer\ExecutionState; $this->execution->shouldReceive('isCancelled') ->once() ->andReturn('cancel'); $this->execution->shouldReceive('hasEnded') ->once() ->andReturn('ended'); $this->execution->shouldReceive('isSuspended') ->once() ->andReturn('suspended'); $this->execution->shouldReceive('getVariables') ->once() ->andReturn($this->expectedVariables); $this->execution->shouldReceive('getId') ->once() ->andReturn($this->expectedId); $this->fixture->captureExecution($this->execution); } public function testIsCancelled() { $actual = $this->fixture->isCancelled(); $this->assertEquals('cancel', $actual); } public function testHasEnded() { $actual = $this->fixture->hasEnded(); $this->assertEquals('ended', $actual); } public function testIsSuspended() { $actual = $this->fixture->isSuspended(); $this->assertEquals('suspended', $actual); } public function testGetVariables() { $actual = $this->fixture->getVariables(); $this->assertEquals($this->expectedVariables, $actual); } public function testGetId() { $actual = $this->fixture->getId(); $this->assertEquals($this->expectedId, $actual); } }
In setUp(), we condition the mock object to return the values to be captured. Then we call the production captureExecution().
Then, each of the tests calls the appropriate production retrieval function, and we compare to the expected value. In this case all tests pass without changing any production code. Let’s finish out our test cases, writing production code as needed.
getVariables, when there are no variables, returns an empty array. We’ll skip this test because we know Zeta Workflow returns an array and we pass it through.
getVariable() returns the variable value or null if no such variable.
public function testGetVariableMissingReturnsNull() { $actual = $this->fixture->getVariable('bogus'); $this->assertTrue(null === $actual); } public function testGetVariableFirst() { $actual = $this->fixture->getVariable('one'); $this->assertTrue(1 === $actual); } public function testGetVariableLast() { $actual = $this->fixture->getVariable('two'); $this->assertTrue(2 === $actual); }
The production code:
public function getVariable($key) { return (is_array($this->_variables) && array_key_exists($key, $this->_variables)) ? $this->_variables[$key] : null; }
hasVariable() returns true or false.
public function hasVariable($key) { return (is_array($this->_variables) && array_key_exists($key, $this->_variables)) ? true : false; }
We have covered all our tests cases. We have a nice, tight, Execution State object with a clear single responsibility. Once we add the docblock comments, we’re done.
Next up: Execution State Registry.