抱歉标题混乱,我正在尝试将单元测试添加到我的(可能)臭代码库中。 我有类似以下课程的内容:
namespace App\Service;
use App\Messages\ItemMessage;
use App\Messages\LocationMessage;
use App\Messages\Message;
use App\Settings\Settings;
class MassMessenger
{
protected static function sendMessagesForDay (int $day, string $content, string $title, Message $messageType): void
{
$posts = \App\Repository\Post::getPostForDay($day);
foreach ($posts as $post) {
$message = new $messageType($post->getId(), $content, $title);
$message->triggerMail();
}
}
public static function sendItem (int $day, Settings $settings): void
{
$messageType = new ItemMessage(0, '', '');
self::sendMessagesForDay($day, $settings->getEmailContent(), $settings->getEmailTitle(), $messageType );
}
public static function sendLocation (int $day, Settings $settings): void
{
$messageType = new LocationMessage(0, '', '');
self::sendMessagesForDay($day, $settings->getEmailContent(), $settings->getEmailTitle(), $messageType );
}
}
Message类是LocationMessage和ItemMessage继承的抽象类。 我想测试公共方法,我不太关心模拟存储库,所以它不是一个完全干净的单元测试。不过,我也不想创建一个集成测试,在其中询问 PHPMailer 消息是否已通过。
所以现在我对如何设计这些测试感到困惑,我尝试了一下:
<?php
namespace Tests\Service;
use App\Messages\ItemMessage;
use App\Service\MassMessenger;
use App\Settings\Settings;
use PHPUnit\Framework\TestCase;
class MassMessengerTest extends TestCase
{
public function testSendItem()
{
$day = 1;
$settings = $this->createMock(Settings::class);
$settings->method('getEmailContent')->willReturn('Test Content');
$settings->method('getEmailTitle')->willReturn('Test Title');
$messageMock = \Mockery::mock( ItemMessage::class);
$messageMock->shouldReceive('triggerMail')->once();
MassMessenger::sendItem($day, $settings);
// The test will fail if triggerMail is not called
}
protected function tearDown(): void
{
parent::tearDown();
\Mockery::close();
}
}
它(当然)没有检测到模拟,可能是因为我没有用它做任何事情
Mockery\Exception\InvalidCountException : Method triggerMail(<Any Arguments>) from Mockery_0_App_Messages_ItemMessage should be called
当我重载该类(例如模拟硬依赖项)时,我收到此错误:
TypeError : App\Service\MassMessenger::sendMessagesForDay(): Argument #4 ($messageType) must be of type App\Messages\Message, App\Messages\ItemMessage given, called in /xxx/src/Service/MassMessenger.php on line 24
此外,我很愿意听到任何关于如何消除代码异味的建议,我仍在学习软件设计模式,但还没有找到正确的模式来创建在这里可以很好测试的代码。
我还尝试通过模拟创建的消息依赖项来直接测试 sendMessagesForDay 函数,该消息依赖项独立工作但与其他测试一起工作,但失败了
Mockery\Exception\RuntimeException: Could not load mock App\Messages\Message, class already exists
并添加
* @runInSeparateProcess
* @preserveGlobalState disabled
只是让测试悄然失败。我认为我遇到这些问题是因为 sendMessage 创建了模拟对象的新实例。由于测试这个已经很难了,我想我最好重构该类以使其更好地可测试。
任何建议都非常感谢,谢谢!
为了“消除异味”并使类单元可测试,您应该对所需的类使用依赖注入,而不是在它们上调用静态方法。 我不知道在您的应用程序中创建
MassMessenger
类时注入依赖项是否更有意义,或者将它们注入到每个静态方法中是否更有意义。
第一个示例可能看起来像(未经测试)
class MassMessenger
{
protected $post;
protected $itemMessage;
protected $locationMessage;
public function __construct($post, $itemMessage, $locationMessage = null)
{
$this->post = $post;
$this->itemMessage = $itemMessage;
$this->locationMessage = $locationMessage;
}
protected function sendMessagesForDay (int $day, string $content, string $title, Message $message): void
{
$posts = $post->getPostForDay($day);
foreach ($posts as $post) {
$message->triggerMail($post->getId(), $content, $title);
}
}
public function sendItem (int $day, Settings $settings): void
{
$this->sendMessagesForDay($day, $settings->getEmailContent(), $settings->getEmailTitle(), $this->itemMessage );
}
public function sendLocation (int $day, Settings $settings): void
{
$this->sendMessagesForDay($day, $settings->getEmailContent(), $settings->getEmailTitle(), $this->locationMessage );
}
}
那么你的测试可能看起来像这样
class MassMessengerTest extends TestCase
{
public function testSendItem()
{
$day = 1;
$settings = $this->createMock(Settings::class);
$settings->method('getEmailContent')->willReturn('Test Content');
$settings->method('getEmailTitle')->willReturn('Test Title');
$messageMock = \Mockery::mock( ItemMessage::class);
$messageMock->shouldReceive('triggerMail')->once();
$messenger = new MassMessenger($post, $messageMock);
$messenger->sendItem($day, $settings);
// The test will fail if triggerMail is not called
}
protected function tearDown(): void
{
parent::tearDown();
\Mockery::close();
}
}
一般情况