【问题标题】:How to unit test Symfony controllers如何对 Symfony 控制器进行单元测试
【发布时间】:2018-09-19 14:14:18
【问题描述】:

我正在尝试使用 Codeception 在测试工具中获取 Symfony 控制器。每个方法的开头如下:

public function saveAction(Request $request, $id)
{
    // Entity management
    /** @var EntityManager $em */
    $em = $this->getDoctrine()->getManager();

    /* Actual code here
    ...
    */
}

public function submitAction(Request $request, $id)
{
    // Entity management
    /** @var EntityManager $em */
    $em = $this->getDoctrine()->getManager();

    /* 200+ lines of procedural code here
    ...
    */
}

我试过了:

$request = \Symfony\Component\HttpFoundation\Request::create(
    $uri, $method, $parameters, $cookies, $files, $server, $content);

$my_controller = new MyController();
$my_controller->submitAction($request, $id);

从我的单元测试来看,但似乎还有很多我不知道 Symfony 在后台执行的其他设置。每次我找到一个丢失的对象并对其进行初始化时,都会有另一个对象在某个时候失败。

我也尝试过从 PhpStorm 逐步完成测试,但是 PhpUnit 有一些输出导致 Symfony 在接近我要测试的代码之前死掉,因为它无法启动 $_SESSION发生了输出。我认为这不会发生在命令行中,但我还没有真正接近到可以说出来。

如何在单元测试中简单且可扩展地运行此代码?


一点背景:

我继承了这个代码。我知道它很脏而且有异味,因为它在控制器中执行模型逻辑。我知道我要求的不是“纯”单元测试,因为它几乎涉及整个应用程序。

但我需要能够自动运行这个“小”(200 多行)代码。代码应该在不超过几秒钟的时间内运行。我不知道要多久,因为我一直无法独立运行它。

目前,通过网站运行此代码的设置时间很长,也很复杂。该代码不会生成网页,它基本上是生成文件的 API 调用。在进行编码更改时,我需要能够在短时间内生成尽可能多的这些测试文件。

代码就是这样。能够对其进行更改是我的工作,而目前我什至无法在每次运行它的情况下都没有巨大的开销。在不知道它在做什么的情况下对其进行更改是不负责任的。

【问题讨论】:

  • /* 200+ lines of procedural code here 我认为是时候将一些业务逻辑卸载到模型中了,你猜怎么着,它会更容易测试。控制器是 MVC 的粘合剂,它们并不意味着做出超出模型将它们绑定在一起并将其放在视图中所需的决策......
  • @ArtisticPhoenix 目标:1. 不破坏任何东西,2. 了解代码在做什么,3. 重构以便任何人都能理解它在做什么。我几乎需要对所有这些进行单元测试。

标签: php unit-testing symfony phpunit codeception


【解决方案1】:

您的部分问题是,当您编写从 Symfony 的基本控制器或新的 AbstractController 扩展的控制器时,它将从容器中加载其他依赖项。您必须在测试中实例化这些服务并将它们传递给容器,然后您可以像这样在控制器中设置:

$loader = new Twig_Loader_Filesystem('/path/to/project/app/Resources/views');
$twig = new Twig_Environment($loader, array(
    'cache' => '/path/to/app/var/cache/templates',
));

# ... other services like routing, doctrine and token_storage

$container = new Container();
$container->set('twig', $twig);

$controller = new MyController();
$controller->setContainer($container);

或模拟它们,这会使您的测试几乎不可读,并且在您对代码所做的每一次更改时都会中断。

如您所见,这并不是真正的单元测试,因为您需要通过调用 $this->get()/$this->container->get() 或间接调用从容器中提取的所有服务,例如通过控制器中的辅助方法,例如getDoctrine().

如果您不按照在生产中使用的相同方式配置服务,这不仅会很乏味,而且您的测试可能没有多大意义,因为它们可能在您的测试中通过但在生产中失败。

您的问题的另一部分是您的 sn-p 中的注释:

这里有 200 多行程序代码

如果没有看到代码,我可以告诉你正确的单元测试几乎是不可能的,也不值得。

简短的回答是,你不能。

我的建议是写Functional Tests using WebTestCase 或类似Selenium with CodeCeption 并通过用户界面间接测试您的控制器。

一旦测试覆盖了操作的(主要)功能,您就可以开始重构控制器,将其拆分为更易于测试的小块和服务。对于那些新类,单元测试是有意义的。当您的功能测试(再次)变为绿色时,您将知道网站何时像您更改之前一样再次运行。理想情况下,您不需要更改这些第一个测试,因为它们仅通过浏览器查看您的网站,因此不受您所做的任何代码更改的影响。请注意不要对模板和路由进行更改。

【讨论】:

  • 如果我理解正确,Selenium 在浏览器中工作并且只测试返回给浏览器的内容。此代码正在文件系统上创建文件。这段代码不是我写的;我继承了它。这个特定的测试不会真正受益于知道网络呼叫结果,即{"error":false}。我需要对生成的文件进行更改,因此知道调用没有崩溃和烧毁是不够的。事实上,我只需要生成一个文件即可开始。我无法为每 30 秒的代码更改进行半小时的测试设置过程。
  • 在这种情况下,您可能应该编写一个执行命令的测试,例如像这样exec('bin/console my:command'); 然后检查生成文件的输出
  • 抱歉,我顺便读了您的评论,并假设控制器是从命令内部执行的。我猜你的意思是控制器调用一个命令,然后返回一个带有该调用结果的 JSON 响应。在这种情况下,我仍然会使用 Selenium/WebTestCase 之类的东西通过浏览器触发操作,然后针对生成的文件进行断言,例如如果它包含我期望的数据,而不是(仅)针对触发操作后网页上显示的内容。
  • 是的,我还没来得及将它作为命令运行(我还有另一个通常由 cron 作业调用的命令),但我看不出我会如何通过在所有 HTTP 变量中。我认为控制器不会调用命令。它只是做了一堆事情(可能是 200 件事情)然后返回。目前我不需要 100% 的覆盖率,只需要一个生成文件的路径。一旦我有部分覆盖,我可以只重构被覆盖的部分,将它分解成更小的方法,然后尝试扩展测试以覆盖更多。一旦我可以运行测试,我就擅长重构部分。
  • 虽然如果你应该对控制器使用单元测试是一个不同的讨论,但说你不能这样做是不正确的。即使您从 AbstractController 扩展(您可以通过构造函数或操作参数简单地使用 DI 来避免这种情况),您仍然可以使用 setContainer 函数在需要的地方创建一个容器模拟并将其设置在您的容器中。因此,无论您选择哪种解决方案,它都可以作为单元测试进行测试。有关具体问题,另请参阅下面我的回答。
【解决方案2】:

对于仍然遇到这个问题的人来说,通过控制器函数参数引入依赖注入,为控制器编写单元测试变得更加容易。

例如。如果你的代码写得不同:

public function save(Request $request, DocumentManagerInterface $em, int $id): RedirectResponse
{
    /* Actual code here
    ...
    */
}

您的 UNIT 测试可以简单地做到这一点:

public function testSave(): void
{
    $em = $this->createMock(EntityManagerInterface::class);
    // test calls on the mock

    $controller = new XXXController();
    $response = $controller->save($em);

    // response assertions
}

另请注意,如果您使用的是存储库,则可以直接注入存储库,前提是您从服务扩展存储库。

提示:您可能想查看 Symfony 最佳实践并使用 ParamConverter 而不是 $id (https://symfony.com/doc/current/best_practices/index.html)

【讨论】:

  • 我认为需要将 Symfony 更新到最新版本,或者至少比我的版本更新。
  • 如果我的代码写得不同?您是否建议我在添加单元测试之前更改代码?
  • 您的选择。我还在上面对另一个问题发表了评论。您可以使用 setContainer 来模拟容器,因此通过提供 ManagerRegistry 来模拟 getDoctrine 的内部逻辑,然后模拟 getManager 响应以提供模拟的 DocumentManager,但更改代码可能会减少工作量。我猜(但不确定)从 Symfony 3 开始你就可以做到这一点,我希望你已经在那里,因为 Symfony 2 很快就要结束生命了。
  • 重点是在我尝试更新组件并可能破坏某些东西之前,我想尽可能多地覆盖遗留代码。单元测试会告诉我需要修复什么。
【解决方案3】:

我发现只需几行简短的代码就可以将 Symfony 纳入测试工具:

// Load the autoloader class so that the controller can find everything it needs
//$loader = require 'app/vendor/autoload.php';
require 'app/vendor/autoload.php';

// Create a new Symfony kernel instance
$kernel = new \AppKernel('prod', false);
//$kernel = new \AppKernel('dev', true);
// Boot the kernel
$kernel->boot();
// Get the kernel container
$container = $kernel->getContainer();
// Services can be retrieved like so if you need to
//$service = $container->get('name.of.registered.service');

// Create a new instance of your controller
$controller = new \What\You\Call\Your\Bundle\Controller\FooBarController();
// You MUST set the container for it to work properly
$controller->setContainer($container);

在此代码之后,您可以测试控制器上的任何公共方法。当然,如果您正在测试生产代码(我必须这样做;我的开发代码的工作方式完全不同,因为代码库编写得非常糟糕)请注意您可能正在接触数据库、进行网络调用等。

不过,好处是您可以开始对控制器进行代码覆盖,以了解它们为何无法正常工作。

【讨论】:

  • 这不是单元测试,因此无法回答问题
猜你喜欢
  • 2012-04-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-31
  • 2018-08-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多