I have a Logger
interface that accepts a SplFileObject
in the constructor to use as the file for that particular log. There is also a log($timestamp, $message)
method available to actually do the logging. In my first implementation when instantiating a new object and passing a read-only SplFileObject
an exception should be thrown. I wrote up an appropriate unit test:
<?php
class FileLoggerTest extends PHPUnit_Framework_TestCase {
/**
* @expectedException \InvalidArgumentException
*/
public function testReadOnlyFileObjectFailure() {
$file = '/Library/WebServer/Documents/sprayfire/tests/mockframework/logs/test-log.txt';
$LogFile = new \SplFileObject($file);
$Logger = new \libs\sprayfire\logger\FileLogger($LogFile);
$Logger->log('test', 'something');
}
}
?>
Normally I would have a method producing the directory name but when I started encountering problems I changed it to an absolute path to rule out that as the cause.
And here's the implementation:
namespace libs\sprayfire\logger;
use \SplFileObject as SplFileObject;
use \InvalidArgumentException as InvalidArgumentException;
use libs\sprayfire\logger\Logger as Logger;
/**
* @brief A framework implemented class that adds a timestamp log message to
* the end of an injected file.
*/
class FileLogger implements Logger {
/**
* @brief A SplFileObject that should be used to write log messages to.
*
* @property $LogFile
*/
protected $LogFile;
/**
* @param $LogFile SplFileObject that should have log messages written to
*/
public function __construct(SplFileObject $LogFile) {
$this->LogFile = $LogFile;
$this->throwExceptionIfFileNotWritable();
}
/**
* @throws InvalidArgumentException
*/
protected function throwExceptionIfFileNotWritable() {
$isWritable = $this->LogFile->isWritable();
if (!$isWritable) {
throw new InvalidArgumentException('The passed file, ' . $this->LogFile->getPathname() . ', is not writable.');
}
}
/**
* @param $timestamp A formatted timestamp string
* @param $message The message string to log
* @return boolean true if the message was logged, false if it wasn't
*/
public function log($timestamp, $message) {
if (!isset($timestamp) || empty($timestamp)) {
$timestamp = 'No timestamp given';
}
if (!isset($message) || empty($message)) {
$message = 'Attempting to log an empty message';
}
$separator = ' := ';
$message = $timestamp . $separator . $message;
$wasWritten = $this->LogFile->fwrite($message);
if (!isset($wasWritten)) {
return false;
}
return true;
}
}
// End FileLogger
The problem is that the test passes and I can tell by the code coverage generated by the test that isWritable()
returns true and SplFileObject::fwrite()
on a readonly object returns a non-null value as well.
The really, really weird part of this is that the very same code ran in a non-unit test example fails, just as it should.
$logFile = '/Library/WebServer/Documents/sprayfire/tests/mockframework/logs/test-log.txt';
$SplFile = new \SplFileObject($logFile);
$Logger = new \libs\sprayfire\logger\FileLogger($SplFile);
Running this from index.php
results in xdebug showing an uncaught InvalidArgumentException
from FileLogger
with the expected message that the file passed is not writable. This is completely baffling, the same exact code is being ran in both situations, yet the code inside the unit test is "failing" and the non-unit tested code is performing as expected.
- Yes, the file exists.
SplFileObject
would throw an exception if it didn't. - The very exact same code is being ran in both situations, other code that is being ran includes setting up 2 constants, a file directory and a shortcut to
DIRECTORY_SEPARATOR
, and setting up class autoloading. But, again, this is happening exactly the same in both situations and would result in a failure long before this unit test is actually ran. - Help!
Looking at it now the problem seems relatively simple. PHP is running under the _www
user and phpunit is running as the user that installed it. These users have different permissions, which makes perfect sense. If you somehow are encountering this problem I suggest you look at edorian's answer and re-evaluate how you are writing your unit tests.