1

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.

4

1 回答 1

5

首先:

对于单元测试,有SplTempFileObject extends SplFileObject.

您通常不需要为此在磁盘上创建真实文件,因为它很慢;)

对于isReadable/isWriteable检查您的 phpunit 测试,您通常会在磁盘上创建以太创建非可读/可写文件或在适用的情况下使用vfsStreamWrapper。它也适用于SplFileObject.

我们的问题:

在测试中,最后一行应该被删除。你除了构造期间的异常,所以让我们摆脱它;)

我发现奇怪的是你在那里有一条绝对路径。您的根文件夹结构真的以 开头'/Library/WebServer/Documents/吗?主要是我很困惑,因为这意味着您的测试位于“WebServer”目录中。无论如何..继续前进:

“为我工作”

附件是一个独立版本的测试,它可以正常工作并抛出预期的异常。

除了告诉您问题似乎出在您的设置中的其他地方之外,我看不出我可以在这里做多少。也许尝试InvalidArgumentException不使用/或单独尝试测试/使用新创建的文件。

PHPUnit 不会干扰文件处理功能,所以除此之外我不知道。下面的示例代码对您有用吗?:)

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;
    }
}
于 2011-12-26T20:34:48.733 回答