【问题标题】:PHPUnit and SplFileObject returning true isWritable on read-only objectPHPUnit 和 SplFileObject 在只读对象上返回 true isWritable
【发布时间】:2011-12-26 16:03:59
【问题描述】:

我有一个Logger 接口,它在构造函数中接受SplFileObject 以用作该特定日志的文件。还有一个log($timestamp, $message) 方法可用于实际进行日志记录。在我的第一个实现中,当实例化一个新对象并传递一个只读的SplFileObject 时,应该抛出一个异常。我写了一个合适的单元测试:

<?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');
    }

}
?>

通常我会有一个生成目录名称的方法,但是当我开始遇到问题时,我将其更改为绝对路径以排除其原因。

下面是实现:

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

问题是测试通过了,我可以通过测试生成的代码覆盖率判断 isWritable() 返回 true,而只读对象上的 SplFileObject::fwrite() 也返回非空值。

这其中非常非常奇怪的部分是,在非单元测试示例中运行的相同代码失败了,正如它应该的那样。

$logFile = '/Library/WebServer/Documents/sprayfire/tests/mockframework/logs/test-log.txt';
$SplFile = new \SplFileObject($logFile);
$Logger = new \libs\sprayfire\logger\FileLogger($SplFile);

index.php 运行它会导致xdebug 显示来自FileLogger 的未捕获InvalidArgumentException,并带有预期的消息,即传递的文件不可写。这完全令人费解,在两种情况下都在运行完全相同的代码,但单元测试中的代码“失败”,而未经单元测试的代码却按预期执行。


  1. 是的,文件存在。如果没有,SplFileObject 会抛出异常。
  2. 在这两种情况下运行完全相同的代码,正在运行的其他代码包括设置 2 个常量、一个文件目录和DIRECTORY_SEPARATOR 的快捷方式,以及设置类自动加载。但是,同样,这在两种情况下发生的情况完全相同,并且会在实际运行此单元测试之前很久就导致失败。
  3. 救命!

现在看问题似乎比较简单。 PHP 在_www 用户下运行,phpunit 以安装它的用户身份运行。这些用户具有不同的权限,这非常有意义。如果您以某种方式遇到此问题,我建议您查看 edorian 的答案并重新评估您是如何编写单元测试的。

【问题讨论】:

  • 可能测试和非测试代码以不同的用户运行,而测试用户有写权限?
  • @FrancisAvila 不幸的是,测试文件实际上从未写入任何内容。 SplFileObject::fwrite() 的返回值是非空值,但单元测试中从未写入任何字节。
  • 测试用例中fwrite()的返回值是多少?使用 is_writable()fopen()fwrite()(非 SPL)得到相同的结果吗?
  • @FrancisAvila 我在文件记录器测试中添加了一个断言,文件路径上的is_writable() 返回 false 并且断言失败。也许运行测试的用户毕竟拥有不同的权限......这是目前唯一有意义的事情。 fwrite() 在单元测试中返回 0
  • 假设您使用的是 unix,您可以在 isWriteable() 调用中使用 posix_getpwuid(posix_geteuid()) 确定正在运行的用户。 ls -le您的 test-log.txt 文件以查看权限和 ACL(如果有)。

标签: php phpunit spl


【解决方案1】:

首先:

对于单元测试,有SplTempFileObject extends SplFileObject

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

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

我们的问题:

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

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

“为我工作”

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

除了告诉您问题似乎出在您的设置中的其他地方之外,我看不出我可以在这里做什么。也许尝试不带/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;
    }
}

【讨论】:

  • 这并没有完全解决我遇到的问题,而是让我重新评估了我是如何编写单元测试的。这值得赞成并被接受为答案。
猜你喜欢
  • 2017-06-27
  • 1970-01-01
  • 2012-04-25
  • 1970-01-01
  • 1970-01-01
  • 2015-01-30
  • 1970-01-01
  • 2021-01-26
  • 2021-02-15
相关资源
最近更新 更多