如何对Symfony控制器进行单元测试

8

我正在尝试使用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 。我不认为这种情况发生在命令行,但我还没有足够接近可以确定。
我该如何简单而可扩展地在Unit Test中运行此代码?
一些背景:
我继承了此代码。我知道它很肮脏,因为它在控制器中执行模型逻辑。我知道我所要求的不是“纯”单元测试,因为它触及了几乎整个应用程序。
但是我需要能够自动运行这个“小”(200+行)代码。代码应该在不到几秒钟内运行。我不知道需要多长时间,因为我从未能够独立运行它。
目前,通过网站运行此代码的设置时间非常长,并且也很复杂。这段代码不会生成网页,它基本上是一个API调用,生成文件。我需要能够在短时间内生成任意数量的这些测试文件,因为我正在进行编码更改。
代码就是它所是的。我的工作是能够对其进行更改,而目前我甚至无法在没有巨大开销的情况下运行它。不知道它在做什么就对它进行更改是不负责任的。

/* 这里有200多行的过程式代码,我认为现在是时候将一些业务逻辑转移到模型中了,而且猜猜看,这样做会更容易测试。控制器是MVC的粘合剂,它们不应该做出超出将模型绑定在一起并放入视图所需的决策...*/ - ArtisticPhoenix
1
@ArtisticPhoenix 的目标:1. 不要破坏任何东西,2. 理解代码正在做什么,3. 重构以便任何人都能理解它正在做什么。我几乎需要为所有这些编写单元测试。 - CJ Dennis
3个回答

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())从容器中提取的所有服务。
如果您没有像在生产中使用的那样配置服务,这不仅很繁琐,而且您的测试可能并不是非常有意义,因为它们可以通过您的测试但在生产中失败。
您问题的另一个部分是您片段中的注释:
200多行的过程性代码在这里
没有看到代码,我可以告诉您适当地对其进行单元测试几乎是不可能的,也不值得。
简短的答案是,你不能。
我建议的是要么编写使用WebTestCase进行功能测试,要么使用CodeCeption中的Selenium,并通过UI间接地测试您的控制器。
一旦您有了覆盖操作(主要)功能的测试,就可以开始重构您的控制器,将其拆分为更小的块和易于测试的服务。对于这些新类,单元测试是有意义的。当您的功能测试再次变为绿色时,您将知道网站再次像之前一样工作了。理想情况下,您不需要更改这些第一个测试,因为它们只通过浏览器查看您的网站,因此不受您所做的任何代码更改的影响。只需小心不要引入对模板和路由的更改即可。

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

0

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

例如,如果您的代码编写方式不同:

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

你的单元测试可以这样做:

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而不是$idhttps://symfony.com/doc/current/best_practices/index.html

我理解这需要将Symfony更新到最新版本,或者至少比我现在使用的版本更新。 - CJ Dennis
如果我的代码写得不同?您是建议我在添加单元测试之前更改代码吗? - CJ Dennis
你可以使用 setContainer 来模拟容器,从而通过提供 ManagerRegistry 模拟 getDoctrine 的内部逻辑,然后模拟 getManager 响应以提供模拟的 DocumentManager,但更改代码可能会更少工作。我猜测(但不确定)自 Symfony 3 以来您可以这样做,希望您已经到达那里,因为 Symfony 2 很快就要结束生命周期了。 - Rein Baarsma
关键是在我尝试更新组件并可能破坏某些东西之前,我希望尽可能多地涵盖遗留代码。单元测试将告诉我需要修复的内容。 - CJ Dennis

-1

我发现只需要几行简短的代码就可以将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);

在这段代码之后,您可以测试控制器上的任何公共方法。当然,如果您正在测试生产代码(就像我必须做的那样;因为代码库编写得很糟糕,我的开发代码完全不同),请注意可能会涉及到触及数据库,进行网络调用等。
但是,好处是您可以开始对控制器进行代码覆盖,以了解它们为什么无法正常工作。

这不是一个单元测试,因此它不能回答这个问题。 - Rein Baarsma

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接