【发布时间】: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,并带有预期的消息,即传递的文件不可写。这完全令人费解,在两种情况下都在运行完全相同的代码,但单元测试中的代码“失败”,而未经单元测试的代码却按预期执行。
- 是的,文件存在。如果没有,
SplFileObject会抛出异常。 - 在这两种情况下运行完全相同的代码,正在运行的其他代码包括设置 2 个常量、一个文件目录和
DIRECTORY_SEPARATOR的快捷方式,以及设置类自动加载。但是,同样,这在两种情况下发生的情况完全相同,并且会在实际运行此单元测试之前很久就导致失败。 - 救命!
现在看问题似乎比较简单。 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(如果有)。