
在开发复杂的web应用时,控制器往往会依赖多个服务来处理业务逻辑、数据持久化或外部通信。例如,一个典型的控制器可能包含日志服务、实体管理器、自定义业务服务以及邮件服务等。以下是一个示例控制器 webhookcontroller:
final class WebhookController extends AbstractController
{
private CustomLoggerService $customLogger;
private EntityManagerInterface $entityManager;
private MyService $myService; // 假设此服务调用外部API
private UserMailer $userMailer;
private AdminMailer $adminMailer;
public function __construct(
CustomLoggerService $customLogger,
EntityManagerInterface $entityManager,
MyService $myService,
UserMailer $userMailer,
AdminMailer $adminMailer
) {
$this->customLogger = $customLogger;
$this->myService = $myService;
$this->userMailer = $userMailer;
$this->adminMailer = $adminMailer;
$this->entityManager = $entityManager;
}
/**
* @Route("/webhook/new", name="webhook_new")
*/
public function new(Request $request): Response
{
$uri = $request->getUri();
$this->customLogger->info("new event uri " . $uri);
$query = $request->query->all();
if (isset($query['RessourceId'])) {
$id = $query['RessourceId'];
// MyService::getInfos() 调用外部API
$event = $this->myService->getInfos($id);
$infoId = $event->infoId;
$this->customLogger->info("new info id " . $infoId);
$userRepo = $this->entityManager->getRepository(User::class);
$user = $userRepo->findOneByEventUserId((int)$event->owners[0]);
$this->userMailer->sendAdminEvent($event, $user);
$this->customLogger->info("new mail sent");
} else {
$this->adminMailer->sendSimpleMessageToAdmin("no ressource id", "no ressource id");
}
return new JsonResponse();
}
}在测试此类控制器时,我们面临以下挑战:
Symfony的测试环境提供了一种优雅的解决方案:通过其依赖注入容器来覆盖特定的服务。这意味着我们可以在测试运行时,将容器中注册的某个服务实例替换为我们预先创建的模拟对象。这样,当控制器被实例化时(由Symfony容器自动完成),它将接收到我们注入的模拟服务,而不是真实的服务。
实现这一策略的关键在于:
下面将详细介绍如何在 WebTestCase 中模拟 MyService 并将其注入到 WebhookController 中。
首先,我们需要修改 config/services_test.yaml 文件,将 MyService 标记为 public。这使得在测试环境中,该服务可以被测试代码获取和覆盖。
# config/services_test.yaml
App\Service\MyService:
public: true说明: public: true 仅在 test 环境中生效,不会影响 dev 或 prod 环境的服务行为。它的作用是允许测试代码通过 self::$container-youjiankuohaophpcnget(MyService::class) 获取到 MyService 实例,并且更重要的是,允许我们通过 self::$container->set(MyService::class, $mockedService) 来覆盖它。
在你的功能测试类中,使用 PHPUnit 的 createMock 方法来创建一个 MyService 的模拟对象,并定义其行为。
// src/Tests/Controller/WebhookControllerTest.php
use App\Service\MyService;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\KernelBrowser;
class WebhookControllerTest extends WebTestCase
{
// ... 其他测试辅助方法或 trait
public function testNewWebhookWithResourceId(): void
{
// 确保每次测试开始时内核是关闭的,以获得干净的容器状态
self::ensureKernelShutdown();
/** @var KernelBrowser $client */
$client = static::createClient(); // 使用 static::createClient() 创建客户端,它会启动内核并提供一个客户端实例
// 创建 MyService 的模拟对象
$myService = $this->createMock(MyService::class);
// 定义模拟对象的行为:当 getInfos 方法被调用一次时,返回一个预设的数组
$myService->expects($this->once())
->method("getInfos")
->willReturn((object)[ // 返回一个对象以模拟原始服务返回的对象结构
'infoId' => 'mockedInfoId',
'owners' => [123]
]);
// ... 接下来是步骤三和步骤四
}
}说明:
这是关键一步。在创建了模拟对象之后,我们需要将其注入到 Symfony 的测试容器中,替换掉原有的 MyService 实例。
// 承接上一步的代码...
// 确保容器已启动,并且可以访问
static::getContainer()->set(MyService::class, $myService);
// ... 接下来是步骤四说明:
最后,使用 WebTestCase 提供的客户端发起HTTP请求,并验证控制器的行为和响应。
// 承接上一步的代码...
// 发起 HTTP 请求
$client->request('GET', '/webhook/new/?RessourceId=1111');
// 验证响应状态码
$this->assertResponseIsSuccessful();
// 验证响应内容(如果控制器返回 JSON 响应)
$this->assertJsonStringEqualsJsonString('{}', $client->getResponse()->getContent());
// 进一步验证,例如检查数据库状态、日志记录等
// 如果你需要检查日志服务是否被调用,你也可以模拟 CustomLoggerService
}
}说明:
将以上所有步骤整合,一个完整的 WebhookControllerTest 示例如下:
<?php
namespace App\Tests\Controller;
use App\Entity\User; // 假设 User 实体存在
use App\Service\MyService;
use App\Service\CustomLoggerService; // 如果也需要模拟日志服务
use App\Service\UserMailer; // 如果也需要模拟邮件服务
use App\Service\AdminMailer; // 如果也需要模拟邮件服务
use Doctrine\ORM\EntityManagerInterface; // 如果需要模拟实体管理器
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\KernelBrowser;
class WebhookControllerTest extends WebTestCase
{
// 可以添加 LoginTrait 或其他辅助 trait
public function testNewWebhookWithResourceIdSuccessfullyProcessesEvent(): void
{
// 1. 确保每次测试开始时内核是关闭的,以获得干净的容器状态
self::ensureKernelShutdown();
/** @var KernelBrowser $client */
$client = static::createClient(); // 使用 static::createClient() 启动内核并创建客户端
// 2. 创建 MyService 的模拟对象并定义其行为
$mockedMyService = $this->createMock(MyService::class);
$mockedMyService->expects($this->once())
->method("getInfos")
->with('1111') // 验证 getInfos 是否被正确参数调用
->willReturn((object)[ // 模拟 MyService 返回的对象结构
'infoId' => 'mocked_info_id_123',
'owners' => [456] // 模拟用户ID
]);
// 3. 在测试容器中覆盖 MyService
// 确保 MyService 在 config/services_test.yaml 中设置为 public
static::getContainer()->set(MyService::class, $mockedMyService);
// 如果也需要模拟 EntityManager 或其 Repository
// 示例:模拟 UserRepository
$mockedUser = $this->createMock(User::class);
// ... 定义 $mockedUser 的行为,例如 getId() 等
$mockedUserRepository = $this->createMock(\Doctrine\ORM\EntityRepository::class); // 实际应该是 UserRepository
$mockedUserRepository->expects($this->once())
->method('findOneByEventUserId')
->with(456)
->willReturn($mockedUser);
$mockedEntityManager = $this->createMock(EntityManagerInterface::class);
$mockedEntityManager->expects($this->once())
->method('getRepository')
->with(User::class)
->willReturn($mockedUserRepository);
static::getContainer()->set(EntityManagerInterface::class, $mockedEntityManager);
// 如果也需要模拟邮件服务,例如 UserMailer
$mockedUserMailer = $this->createMock(UserMailer::class);
$mockedUserMailer->expects($this->once())
->method('sendAdminEvent'); // 验证邮件发送方法被调用
static::getContainer()->set(UserMailer::class, $mockedUserMailer);
// 4. 发起 HTTP 请求
$client->request('GET', '/webhook/new/?RessourceId=1111');
// 5. 验证响应
$this->assertResponseIsSuccessful();
$this->assertJsonStringEqualsJsonString('{}', $client->getResponse()->getContent());
// 验证 MyService 的 getInfos 方法确实被调用了一次 (由 expects($this->once()) 保证)
// 验证 UserMailer 的 sendAdminEvent 方法确实被调用了一次 (由 expects($this->once()) 保证)
}
public function testNewWebhookWithoutResourceIdSendsAdminMessage(): void
{
self::ensureKernelShutdown();
$client = static::createClient();
// 模拟 AdminMailer
$mockedAdminMailer = $this->createMock(AdminMailer::class);
$mockedAdminMailer->expects($this->once())
->method('sendSimpleMessageToAdmin')
->with("no ressource id", "no ressource id");
static::getContainer()->set(AdminMailer::class, $mockedAdminMailer);
// 发起不带 RessourceId 的请求
$client->request('GET', '/webhook/new/');
$this->assertResponseIsSuccessful();
$this->assertJsonStringEqualsJsonString('{}', $client->getResponse()->getContent());
// 验证 AdminMailer 的 sendSimpleMessageToAdmin 方法被调用
}
}在Symfony中测试包含外部依赖的控制器是一个常见的挑战。通过利用Symfony的测试容器和PHPUnit的模拟功能,我们可以优雅地解决这一问题。核心思想是:将需要模拟的服务在测试配置中标记为 public,然后在测试代码中创建模拟对象,并通过 static::getContainer()->set() 方法将其注入到容器中。这种方法允许我们编写高度隔离、稳定且易于维护的功能测试,确保控制器在各种场景下的正确行为,而无需担心外部系统的影响。
以上就是在Symfony中测试控制器并模拟外部服务依赖的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号