jimdelois/context-specification

A library for PHPUnit to provide support for the BDD ContextSpecification-style testing pattern

0.0.2 2015-03-16 00:08 UTC

This package is not auto-updated.

Last update: 2024-04-22 23:46:37 UTC


README

Build Status

Context Specification Testing Framework

The Context Specification (CS) library is a PHPUnit wrapper intended to promote the use of a CS-style testing pattern. This approach offers the Test-Driven Developer a working syntax which more-closely aligns with how behavior is described in business terms, giving rise to the increasingly popular "Behavior-Driven Design (BDD)" paradigm.

When compared to "traditional" unit testing, Context Specification -style BDD seems negligibly different, if at all. While at first the only deviation appears to be in syntactic semantics, the subtle arrangement of the "different" methods plays a huge part in how functionality is logically organized... and tested.

Namely, this distinction is realized when describing and considering functionality as broken into three parts... (1) The starting point; an initial state; a "Context" in which a particular action is to be taken. (2) The action itself; the "state transition" from the initial state into a new one. (3) The new, final, or resultant state after the state transition is complete.

By testing only one single transitional event or method at a time, and then verifying that everything about the new state is sufficiently expected, in conjunction with having correctly provided an initial state, it can then be assumed that the state transition, itself, is operating correctly. Since a computer program operates by endlessly transitioning from one state to another, we can ensure that a library is operating correctly by testing each transition individually, and from all known initial states. Such a consideration of behavior/functionality will likely lead to greater code coverage, fewer bugs and, when requirements can be expressed in a similar fashion, a more accurately-modeled system.

It's true that the same type of BDD can be practiced with the similar granularity and behavioral coverage without any such wrapper. The hope, however, is that providing a lightweight library and suggesting that a few rules be followed, a developer's tests will read more clearly while also providing a more natural report of test failures (and successes!).

Lastly, there are several approaches that could tighten up the BDD paradigm substantially, similar to some other frameworks in other languages. However, this library is developed specifically for PHPUnit, with tooling in mind. That is to say, this library should work correctly in all extensions of PHPUnit (so long as they aren't explicitly removing default behavior), regardless of any customized TestRunners, Commands, etc, put in place by either a developer or an IDE.

Installation

It is recommended that the Context Specification library be installed via the Composer dependency management tool. Otherwise, the latest compressed file may be downloaded from Github at any time, and any PSR-4 autoloaders will work to include the files therein.

Composer Installation

To your composer.json file, add the following:

"require" : {
	"jimdelois/context-specification": "0.1.*@dev" ,
}

Once you've run composer install or composer update, you should have the right libraries available to you within the {vendordir}/jimdelois/context-specification/src/ContextSpecification/Framework/ directory.

Basic Usage

Depending on the need, simply extend the Concern or StaticConcern classes and begin testing as normal!

If global or isolated functionality is being tested where no state can be injected, inserted, or tracked, then the StaticConcern will be just enough of a harness for this. In either case, it is up to the developer to provide an implementation for the following two abstract methods:

abstract protected function context( );
abstract protected function because( );

The context method is where the starting point, or initial state is established. The because method will be called next, where the SUT or isolated functionality will have its one and only tested state change executed.

If there is a System Under Test (SUT) which requires some initial state, has dependencies which require state prior to injection, or has a resultant internal state that must be checked, then the developer should extend the Concern class and instantiate this system from within:

protected function createSUT( );

By returning the object or system under test from this method, it will automatically be available within the Concern class as $this->sut. It ought be on this object that a Context is set specific to the test, state changes are invoked (in the because method), and assertions are made in observations.

Such observations, whether or not there is a SUT, can be made in standard PHPUnit "test" methods. Any method prefixed with "test" will work, although it is a general recommendation to use the @test annotation built into PHPUnit and then name the observation methods in accordance with traditional CS language, such as should, etc. This will have benefits of a better report, e.g., a --testdox report will read like a regular CS report.

E.g., consider MyTest.php as:

<?php
    use ContextSpecification\Framework\Concern;
    class When_attempting_to_do_something_awesome_given_a_specific_context extends Concern {
        protected function context( ) { ... } // Fill this in!
        protected function because( ) { ... } // Fill this in!

        /**
         * @test
         */
        public function should_really_be_awesome( ) {
            $awesome = true;
            $this->assertTrue( $awesome );
        }

        /**
         * @test
         */
        public function then_it_should_not_be_boring( ) {
            $boring = false;
            $this->assertFalse( $boring );
        }
    }
?>

Then, most reports will read like a traditional CS one (note the verbosity!):

$ ../vendor/bin/phpunit --testdox MyTest.php
PHPUnit 3.7.33 by Sebastian Bergmann.

When_attempting_to_do_something_awesome_given_a_specific_context
[x] should_really_be_awesome
[x] then_it_should_not_be_boring

Additionally, the method decontext is available for optionally "undoing" any of the context establishment. This method is called after each observation, balancing the calls to context. Note that reliance on this method to pass tests for true "units" of functionality is likely the result of a poor test approach or, worse, a bad design (globals?!). Depending on the code we're testing, we may not be able to get away from this, so this function provides an opportunity to "reset" anything between observations. In "integration" testing (not "unit"), the need to do this is more commonplace and arguably much more "acceptable".

Finally, the Context Specification library offers a small amount of extra functionality that is useful in dealing with Exceptions raised from state changes. Just as one should test for all possible positive-scenario contexts, a developer should also test for contexts in which state might be invalid... Typically the target functionality (in the SUT) will be coded to raise an exception in this case. If we provided an invalid input directly into our because method's state change:

protected function because( ) {
	$this->sut->setInteger( 'This is a string but the method expects an INT!' );
}

... The actual invocation of because would cause the Exception to be raised before we're prepared, and PHPUnit would fail immediately. The correct solution is NOT to leave the because method blank and move the state change to the observation! Doing so brings us further back to "standard" TDD practice, wherein the state change is invoked within the same method that asserts - this is precisely what we're trying to avoid. Rather, consider capturing the Exception from within the because method and then evaluating it later. Alternatively, try wrapping the state change in a lambda function and then invoke it later. While the latter approach is only a semantic difference from the "discouraged" one, it's an important distinction which helps to keep our tests consistent and well-organized.

As testing invalid states and ensuring correct Exceptions are raised is a very common occurrence (or should be), the Context Specification framework does the favor of minimizing the amount of work the testing developer has to do to deal with these nuances. By simply calling $this->becauseWillThrowException( );, the Concern will be configured to automatically wrap the because method contents in a lambda, meaning one can define the state change as per usual. Then, two additional methods become available which can be used from within observations to validate and assert the resultant Exceptions: releaseException and captureException.

Examples

Below are a couple of (silly) examples of how we might test a single method on a service using the Context Specification library.

<?php
	use ContextSpecification\Framework\Concern;
	use My\Library\AppService\MyAwesomeAppService;
	use My\Library\Domain\Awesomeness;
	use Phake;

	class When_loading_first_awesomeness_from_service_for_date extends Concern {

		protected $dao_awesomeness;
		protected $date_time;

		// Establish a context in which we'll be testing our functionality
		protected function context( ) {

			$this->date_time = new \DateTime( );

			$this->result_expected = new Awesomeness( 'Today will be AWESOME. Maybe.' );

			$dao_return_array = array(
				$this->result_expected ,
				new Awesomeness( 'Should not be seeing this message' ) ,
				new Awesomeness( 'Three is a charm' )
			);

			$this->dao_awesomeness = Phake::mock( 'My\Library\DAO\AwesomenessInterface' );
			Phake::when( $this->dao_awesomeness )->loadAllByDate( $this->date_time )->thenReturn( $dao_return_array );
		}

		// Setup a System-Under-Test
		protected function createSUT( ) {
			return new MyAwesomeAppService( $this->dao_awesomeness );
		}

		// Execute the functionality; the "state change."
		protected function because( ) {
			$this->result_actual = $this->sut->getFirstAvailableAwesomenessForDate( $this->date_time );
		}

		/**
		 * @test
		 */
		public function should_call_appropriate_method_on_awesomeness_dao( ) {
			Phake::verify( $this->dao_awesomeness )->loadAllByDate( $this->date_time );
			Phake::verifyNoFurtherInteraction( $this->dao_awesomeness );
		}

		/**
         * @test
         */
        public function should_return_correct_awesomeness_object( ) {
        	$this->assertEquals( $this->result_expected , $this->result_actual );
        }
	}
?>

Now, assuming that we've correctly implemented the functionality for the method getFirstAvailableAwesomenessForDate( ) on the MyAwesomeAppService object (only AFTER writing the test, of course!), then we should see something like the following:

$ ../vendor/bin/phpunit --testdox
PHPUnit 3.7.33 by Sebastian Bergmann.

When_loading_first_awesomeness_from_service_for_date
[x] should_call_appropriate_method_on_awesomeness_dao
[x] should_return_correct_awesomeness_object

That's pretty nice! Let's iterate on the functionality of our getFirstAvailableAwesomenessForDate method and ensure that an exception is raised is the input isn't a \DateTime object.

<?php
	use ContextSpecification\Framework\Concern;
	use My\Library\AppService\MyAwesomeAppService;
	use My\Library\Domain\Awesomeness;
	use Phake;

	class When_loading_first_awesomeness_from_service_for_non_date_input extends Concern {

		protected $dao_awesomeness;
		protected $date_time_invalid;

		// Establish a context in which we'll be testing our functionality
		protected function context( ) {

			// This causes the library to trap the contents of "because" into a lambda for later execution.
			$this->becauseWillThrowException( );

			$this->date_time_invalid = 'THIS_IS_A_STRING';
			$this->dao_awesomeness = Phake::mock( 'My\Library\DAO\AwesomenessInterface' );

		}

		// Setup a System-Under-Test
		protected function createSUT( ) {
			return new MyAwesomeAppService( $this->dao_awesomeness );
		}

		// Execute the functionality; the "state change."
		protected function because( ) {
			$this->sut->getFirstAvailableAwesomenessForDate( $this->date_time_invalid );
		}

		/**
		 * @test
		 */
		public function should_raise_invalid_argument_exception( ) {
			$this->setExpectedException( '\InvalidArgumentException' );
			$this->releaseException( );
		}
	}
?>

Note that the framework allowed us to keep a nice, clean because method regardless of the fact that invocation would cause a test-failing error.

Alternatively, one could call captureException which would avoid throwing it and then make it available from within $this->exception so that it may be inspected and used in assertions. It's also possible that one might want to make assertions on the state of $this->sut after having thrown the exception from within.

Ideally, correct implementation will then yield the following when the entire suite is run:

$ ../vendor/bin/phpunit --testdox
PHPUnit 3.7.33 by Sebastian Bergmann.

When_loading_first_awesomeness_from_service_for_date
[x] should_call_appropriate_method_on_awesomeness_dao
[x] should_return_correct_awesomeness_object

When_loading_first_awesomeness_from_service_for_non_date_input
[x] should_raise_invalid_argument_exception

There are some obvious flaws to these two trivial examples, and they are already screaming for a shared parent base Concern. However, the point of intended usage here is arguably more important than proper test design or architecture.

Issues

  • It would be ideal to extend the base PHPUnit Framework with configurable "Test Method Prefixes", just as they currently allow for the adjustment of "Test Suffixes" at the file level. By opening up configurability for method prefixes, we could get away from having to annotate any "specially named CS test methods" using the @test. Extending the PHPUnit_Framework_TestSuite to flag such methods is trivial, and a full swap of this suite for the base is possible with a set of custom Runners and/or Commands - but for the same reason this is a approach that works for introducing additional functionality into PHPUnit, it's a common approach taken by other developers and IDEs... Meaning that if this particular library insisted on also using custom Runners and Commands, it will almost certainly not work in other IDEs or with other extensions of PHPUnit.

    Until such a configuration is accepted into the core PHPUnit framework and eventually propagates to other tooling, we are likely stuck with having to use test* for the test methods or, as recommended, annotate them using @test.

  • PHPUnit's setUp() method is an instance method run before each and every test method or, in this case, "observation." However, setUpBeforeClass() is a static method that runs once per class. In theory, a Context within a Context Specification test case should only be set up once per class (this is arguably one of the largest differences between traditional testing and CS testing - state changes are so clearly separated from assertions and context establishment that there's really no need to ever tearDown and setUp for each observation). This approach is common in other CS frameworks, but the difference is that their base frameworks' "once-per-class" equivalents aren't static methods.

    This is not to suggest that it cannot be done within this library, but it's non necessarily trivial to properly set our Concern up from within the statics. The same goes for tearDown vs. tearDownAfterClass. With that, it's important to realized that a context will be established and a context will be de-contexted before and after every single observation method. What this actually means from a practical standpoint is that there may be performance implications for any deep integration tests that contain multiple observations and complex or latent set ups. For this reason (among several others), it's often wise to logically separate all "unit" suites from "integration" suites.

Next Steps

  • It would be ideal to add additional, class-level annotations such as @concern {Concern name here}, which would allow the tester to group multiple concerns, for the sake of reporting, across several distinct files. Of course, a custom reporter would have to be used to accommodate this, although it's distinctly possible that a listener could sit in the middle and perform some trickery...

    Similarly, grouping at the next-level down could be achieved with some type of @when annotation, as well.

  • Look into addressing the above "issue" concerning once-per-observation establishment of context versus once-per-concern.

Questions/Feedback

Jim DeLois - %%PHPDOC_AUTHOR_EMAIL%%