In the course of developing a new application we've determined we need a Logging system, perhaps to store an audit trail. We're going to assume no current open source Logger library is sufficient for our needs and we are required to develop one from scratch. before we can do anything we need to start figuring out what it needs to do. In other words, how we want it to behave. After consulting with our colleagues we determine at least one fundamental requirement - to log messages to a filesystem.
Rather than immediately jumping into an editor to start coding, we're going to write the specifications first.
Example 5.2. Some Plain Text Specs for a Filesystem Logger
New Filesystem Logger: - should create a new log file if none currently exists - should use an existing log file if one exists without truncating it - should throw Exception if existing log file not writeable
These simple plain text specifications can be translated to PHPSpec by creating a new Context class contain the examples demonstrating these behaviours.
class DescribeNewFilesystemLogger extends PHPSpec_Context
{
public function itShouldCreateCreateNewLogFileIfNoneExists()
{
$this->pending();
}
public function itShouldUseAnExistingLogFileIfOneExistsWithoutTruncatingIt()
{
$this->pending();
}
public function itShouldThrowExceptionIfExistingLogFileNotWriteable()
{
$this->pending();
}
}
This skeleton class has two Pending examples. The pending status simply means they are incomplete or pending completion. If you were to execute this spec from the command line when saved as NewFilesystemLoggerSpec.php (using the alternate filename convention which utilises a "Spec" suffix and omits the "Describe" prefix), the output would look something like:
PPP Finished in 0.0468921661377 seconds 3 examples, 0 failures, 3 pending
The relevant command line target to run PHPSpec would be something like:
phpspec NewFileSystemLoggerSpec
We now have two example methods. Based on the defined specifications, let's fill these in with something useful.
Example 5.3. Specification for a New Filesystem Logger Context
class DescribeNewFilesystemLogger extends PHPSpec_Context
{
public function itShouldCreateCreateNewLogFileIfNoneExists()
{
$file = $this->getTmpFileName();
$logger = new Logger($file);
$this->spec(file_exists($file))->should->beTrue();
}
public function itShouldUseAnExistingLogFileIfOneExistsWithoutTruncatingIt()
{
$file = $this->getTmpFileName();
file_put_contents($file, 'Hello' . "\n");
$logger = new Logger($file);
$this->spec(file_get_contents($file))->shouldNot->beEmpty();
}
public function itShouldThrowExceptionIfExistingLogFileNotWriteable()
{
$file = $this->getTmpFileName();
file_put_contents($file, 'Hello' . "\n");
$this->spec('Logger', $file)->should->throw('Exception');
}
public function after()
{
unlink($this->getTmpFileName());
}
public function getTmpFileName()
{
return sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'logger_tmp_file.log';
}
}
And so we've now turned our plain text specs into coded examples for execution. Of course executing this now will result in an ugly Fatal Error since the Logger class does not yet exist. We'll cross this bridge later on.
Our completed New Filesystem Logger example demonstrates how a Spec is put together.
All Specs are aggregated within a PHPSpec_Context subclass based on the condition of the system being specified
All Context classnames must begin with the term "Describe" to encourage full sentence descriptions
All Example methods in a Context must begin with "itShould", again to encourage full sentence specification text (this might be later shortened to optionally omit "Should" to allow present tense specification language)
A PHPSpec_Context::spec() method is
utilised to prepare any object or scalar value for expectations via
the DSL.
The domain specific langauge (DSL) generally includes an Expectation (should/shouldNot) and a Matcher (beSomething, haveSomething, equals, etc.)
It is almost a rule that you only have one Spec per Example - this ensures each Spec is a single isolated piece of behaviour
You can add any other methods to the class to provide Helper
Methods, e.g. getTmpFileName()
You can use after() and
before() methods to setup common Fixtures for
each Example
You can also use afterAll() and
beforeAll() methods with are run only once
before and after all Examples are executed
Note that any Exceptions or Errors triggered with an Example will be reported but will not interrupt any other tests
With our specification now written up with PHPSpec, we can move on and implement the Logger to its specifications. I'm sure many people will note some paths for refactoring but for now we're only interested in writing the minimum amount of code necessary to pass all our Specs.
Example 5.4. Implementation of the Filesystem Logger
class Logger
{
protected $_file = null;
public function __construct($file)
{
if (!file_exists($file)) {
$f = fopen($file, 'w');
fclose($f);
} elseif (file_exists($file) && is_writeable($file)) {
$this->_file = $file;
} else {
throw new Exception('log file is not writeable');
}
}
}
The next step is deciding what the next behaviour should be so we can write a Spec for it. Maybe you want to add a Logger_Exception class to extend Exception? Maybe the file needs a few more checks? Maybe you want to consider moving file handling to a new subclass or strategy class for composition?
Whatever you would decide - write a spec for it before adding more code. Take small steps and build up your classes iteratively. Remember also not to over-specify. Just because you extract file handling to a new class does not mean you should immediately specify the new class (unless it's valuable enough to warrant it) since the original Specs still cover the effects of a Logger being instantiated with a file. This is not adding new behaviour - it's just changing the implementation of that behaviour transparently.