drap5081683
2011-12-26 16:03 阅读 50
已采纳

PHPUnit和SplFileObject在只读对象上返回true isWritable

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.


  1. Yes, the file exists. SplFileObject would throw an exception if it didn't.
  2. 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.
  3. 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.

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 复制链接分享

1条回答 默认 最新

  • 已采纳
    douyi6818 douyi6818 2011-12-26 20:34

    First off:

    For unit testing there is SplTempFileObject extends SplFileObject.

    You usually don't need to create real files on the disk for that as it's slow anyways ;)

    For an isReadable / isWriteable check in your phpunit tests you generally create ether create non readable / writeable files on the disk or use vfsStreamWrapper where applicable. It also works with SplFileObject.

    Our issue:

    In the test the last line should be removed. You except the exception during construct so let's make away with that ;)

    What I found strange is that you have an absolute path there. Does your root folder structure really start with '/Library/WebServer/Documents/ ? Mainly I'm confused because that would imply that your tests are in a "WebServer" directory. Anyways.. moving on:

    "Works for me"

    Attached is a standalone version of the test that works and throws the exception like expected.

    Apart from telling you that the problem seem to be somewhere else in your setup I don't see much I can do here. Maybe try the InvalidArgumentException without the / or try the test in isolation / with a newly created file.

    PHPUnit doesn't interfere with the file handling functions so I'm out of idea apart from that. Does the sample code below work for you? :)

    phpunit mep.php 
    PHPUnit 3.6.5 by Sebastian Bergmann.
    
    .
    
    Time: 1 second, Memory: 5.00Mb
    
    OK (1 test, 1 assertion)
    
    <?php
    
    class FileLoggerTest extends PHPUnit_Framework_TestCase {
    
        /**
         * @expectedException InvalidArgumentException
         */
        public function testReadOnlyFileObjectFailure() {
            $file = __DIR__."/_files/test-log.txt";
            touch($file);
            chmod($file, 0444);
            $LogFile = new \SplFileObject($file);
            $Logger = new FileLogger($LogFile);
        }
    
    }
    
    
    class FileLogger {
    
        protected $LogFile;
        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;
        }
    }
    
    点赞 评论 复制链接分享

相关推荐