如何对 Symfony 控制器进行单元测试
How to unit test Symfony controllers
我正在尝试使用 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
在任何输出发生后。我不认为这是从命令行发生的,但我还没有真正了解。
如何在单元测试中简单且可扩展地 运行 这段代码?
一些背景知识:
我继承了这段代码。我知道它又脏又臭,因为它在控制器中执行模型逻辑。我知道我要求的不是 "pure" 单元测试,因为它实际上涉及整个应用程序。
但我需要能够 运行 仅此 "small"(200 多行)代码自动执行。代码应该 运行 不超过几秒钟。我不知道多久了,因为我从来没有能够独立 运行 它。
目前,通过网站 运行 此代码的设置时间很长,也很复杂。该代码不会生成网页,它基本上是生成文件的 API 调用。在进行编码更改时,我需要能够在短时间内生成尽可能多的测试文件。
代码就是这样。能够对其进行更改是我的工作,目前我什至无法 运行 每次都需要大量开销。在不知道它在做什么的情况下对其进行更改是不负责任的。
您的部分问题是,当您编写从 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+ lines of procedural code here
在没有看到代码的情况下,我可以告诉您正确地进行单元测试这几乎是不可能的,也不值得。
简短的回答是,你不能。
我推荐的是编写 Functional Tests using WebTestCase or something like Selenium with CodeCeption 并通过 UI 间接测试您的控制器。
一旦您对操作的(主要)功能进行了测试,您就可以开始重构您的控制器,将其拆分为更易于测试的更小的块和服务。对于那些新的 类 单元测试是有意义的。当您的功能测试(再次)变为绿色时,您将知道网站何时像您更改之前一样再次运行。理想情况下,您不需要更改这些首次测试,因为它们仅通过浏览器查看您的网站,因此不会受到您所做的任何代码更改的影响。请注意不要对模板和路由进行更改。
我发现只需几行代码即可让 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);
在此代码之后,您可以在控制器上测试任何 public 方法。当然,如果您正在测试生产代码(我必须这样做;我的开发代码的工作方式完全不同,因为代码库编写得非常糟糕)请注意您可能正在接触数据库、进行网络调用等。
不过,好处是您可以开始对控制器进行代码覆盖,以了解它们为何无法正常工作。
对于仍然被这个问题绊倒的任何人,通过控制器函数参数引入依赖注入,为控制器编写单元测试变得更加容易。
例如。如果您的代码编写方式不同:
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)
我正在尝试使用 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
在任何输出发生后。我不认为这是从命令行发生的,但我还没有真正了解。
如何在单元测试中简单且可扩展地 运行 这段代码?
一些背景知识:
我继承了这段代码。我知道它又脏又臭,因为它在控制器中执行模型逻辑。我知道我要求的不是 "pure" 单元测试,因为它实际上涉及整个应用程序。
但我需要能够 运行 仅此 "small"(200 多行)代码自动执行。代码应该 运行 不超过几秒钟。我不知道多久了,因为我从来没有能够独立 运行 它。
目前,通过网站 运行 此代码的设置时间很长,也很复杂。该代码不会生成网页,它基本上是生成文件的 API 调用。在进行编码更改时,我需要能够在短时间内生成尽可能多的测试文件。
代码就是这样。能够对其进行更改是我的工作,目前我什至无法 运行 每次都需要大量开销。在不知道它在做什么的情况下对其进行更改是不负责任的。
您的部分问题是,当您编写从 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+ lines of procedural code here
在没有看到代码的情况下,我可以告诉您正确地进行单元测试这几乎是不可能的,也不值得。
简短的回答是,你不能。
我推荐的是编写 Functional Tests using WebTestCase or something like Selenium with CodeCeption 并通过 UI 间接测试您的控制器。
一旦您对操作的(主要)功能进行了测试,您就可以开始重构您的控制器,将其拆分为更易于测试的更小的块和服务。对于那些新的 类 单元测试是有意义的。当您的功能测试(再次)变为绿色时,您将知道网站何时像您更改之前一样再次运行。理想情况下,您不需要更改这些首次测试,因为它们仅通过浏览器查看您的网站,因此不会受到您所做的任何代码更改的影响。请注意不要对模板和路由进行更改。
我发现只需几行代码即可让 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);
在此代码之后,您可以在控制器上测试任何 public 方法。当然,如果您正在测试生产代码(我必须这样做;我的开发代码的工作方式完全不同,因为代码库编写得非常糟糕)请注意您可能正在接触数据库、进行网络调用等。
不过,好处是您可以开始对控制器进行代码覆盖,以了解它们为何无法正常工作。
对于仍然被这个问题绊倒的任何人,通过控制器函数参数引入依赖注入,为控制器编写单元测试变得更加容易。
例如。如果您的代码编写方式不同:
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)