8

I'm trying to get a Symfony controller in a test harness using Codeception. Every method starts as follows:

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
    ...
    */
}

I've tried:

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

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

from my unit tests, but it seems there's a lot of other setup I don't know about that Symfony does in the background. Every time I find a missing object and initialise it, there's another one that fails at some point.

I've also tried stepping through the test from PhpStorm, but PhpUnit has some output that causes Symfony to die before it gets anywhere near the code I'm trying to test because it can't start the $_SESSION after any output has occurred. I don't think this happens from the command line, but I'm not really close enough to tell yet.

How can I simply and extensibly run this code in a Unit Test?


A bit of background:

I inherited this code. I know it's dirty and smells because it's doing model logic in the controller. I know that what I'm asking for is not a "pure" unit test because it touches virtually the whole application.

But I need to be able to run just this "small" (200+ lines) bit of code automatically. The code should run in no more than a couple of seconds. I don't know how long because I've never been able to run it independently.

Currently, the setup time to run this code through the website is huge, as well as being complex. The code does not generate a web page, it's basically an API call that generates a file. I need to be able to generate as many of these test files as I like in a short amount of time as I'm making coding changes.

The code is what it is. It's my job to be able to make changes to it and at the moment I can't even run it without a huge overhead each time. It would be irresponsible to make changes to it without knowing what it's doing.

CJ Dennis
  • 4,226
  • 2
  • 40
  • 69
  • `/* 200+ lines of procedural code here` I think it's time to off load some of this business logic to a model, and guess what, it will be easier to test. Controllers are the glue of MVC they are not meant to be making decisions beyond what is need to take the model(s) tie them together and put it in a view... – ArtisticPhoenix Apr 10 '18 at 05:36
  • 1
    @ArtisticPhoenix Goals: 1. don't break anything, 2. understand what the code's doing, 3. refactor so that anyone can understand what it's doing. I pretty much need unit tests for all of these. – CJ Dennis Apr 10 '18 at 05:39

3 Answers3

1

Part of your problem is, that when you write a Controller that extends from Symfony's base Controller or the new AbstractController it will load other dependencies from the container. These service you either have to instantiate in your tests and pass them to a container, that you then set in your controller like this:

$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);

or mock them, which makes your test pretty much unreadable and break on every change you do to the code.

As you can see, this is not really a unit test, because you will need all the services you pull from your container directly by calling $this->get()/$this->container->get() or indirectly, such via the helper methods in the controller, e.g. getDoctrine().

Not only is this tedious if you don't configure the services in the same way as you use in production, your tests might not be very meaningful, as they could pass in your tests but fail in production.

The other part of your problem is the comment inside your snippet:

200+ lines of procedural code here

Without seeing the code I can tell you that properly unit testing this is near impossible and not worth it.

The short answer is, you can't.

What I recommend is either writing Functional Tests using WebTestCase or something like Selenium with CodeCeption and test your controller indirectly through the UI.

Once you have tests covering the (main) functionality of your action, you can start refactoring your controller splitting things into smaller chunks and services that are easier to test. For those new classes unit tests make sense. You will know when the website works again as before your changes, when your Functional Tests are green (again). Ideally you would not need to change these first tests as they only look at your website through the browser and therefore are not affected by any code changes you do. Just be careful to not introduce changes to the templates and the routing.

dbrumann
  • 16,803
  • 2
  • 42
  • 58
  • If I understand correctly, Selenium works in a browser and only tests what's returned to the browser. This code is creating files on the file system. I didn't write this code; I inherited it. This particular test would not really benefit from knowing the web-call result, i.e. `{"error":false}`. I need to make changes to the generated files, so knowing that the call didn't crash and burn is not good enough. In fact, I need to generate just one file to get started. I can't go though a half-hour test setup process for every 30 second code change. – CJ Dennis Apr 10 '18 at 05:48
  • In that case you should probably write a test that executes the command, e.g. like this `exec('bin/console my:command');` and then checks the output from the generated file – dbrumann Apr 10 '18 at 09:47
  • Sorry I read your comment in passing by and assumed the controller was executed from inside a command. I guess you mean the controller calls a command and then returns a JSON-response with the result of that call. In that case I would still use something like Selenium/WebTestCase for triggering the action through the browser and then assert against the generated file, e.g. if it contains the data I expect, and not (solely) against what is displayed on the webpage after triggering the action. – dbrumann Apr 11 '18 at 05:01
  • 1
    Yeah, I hadn't got around to running it as a command (I have another one of those which is normally called by a cron job), but I couldn't see how I'd pass in all the HTTP variables. I don't think the controller calls a command. It just does a bunch of stuff (probably 200 things) and returns. At the moment I don't need 100% coverage, just one path that generates a file. Once I have partial coverage I can refactor just the covered sections, breaking it into smaller methods, then try to extend the tests to cover more. I'm good at the refactoring part once I can run the tests. – CJ Dennis Apr 11 '18 at 05:09
  • Although it is a different discussion if you *should* use unit testing for the controller, it's not correct to say you can't. Even if you extend from the AbstractController (which you can avoid by simply using DI through the constructor or action arguments), you can still use the `setContainer` function to create a container mock where needed and set it in your container. So whichever solution you choose, it is testable as a unit test. See also my answer below for the specific question. – Rein Baarsma Oct 01 '19 at 07:28
0

For anyone still stumbling across this question, it's become easier to write unit tests for the controller with the introduction of Dependency Injection through controller function arguments.

Ex. if your code was written differently:

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

Your UNIT test could simply do this:

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

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

    // response assertions
}

Also note that if you are using a repository, you can inject the repository directly, given that you extend your Repository from the Service.

Tip: You might want to look at the Symfony best practices and use the ParamConverter instead of an $id (https://symfony.com/doc/current/best_practices/index.html)

Rein Baarsma
  • 1,466
  • 13
  • 22
  • I take it that requires updating Symfony to the latest version, or at least a more recent version than I have. – CJ Dennis Oct 01 '19 at 07:28
  • If my code was written differently? Are you suggesting that I change the code before adding unit tests? – CJ Dennis Oct 01 '19 at 07:29
  • Your choice. I've also commented above on the other question. You could use `setContainer` to mock the container and therefore mock the internal logic of `getDoctrine` by giving the ManagerRegistry and then mock the `getManager` response to give a mocked DocumentManager, but it might be less work to change your code. I'm guessing (but not sure) you can do this since Symfony 3 and I'm hoping you're already there, since Symfony 2 is end of life very soon. – Rein Baarsma Oct 01 '19 at 07:40
  • The point is that I want to cover as much of the legacy code as possible before I try updating components and likely breaking things. The unit tests will tell me what I need to fix. – CJ Dennis Oct 01 '19 at 09:38
-1

I have discovered that a few short lines are all that's needed to get Symfony into a test harness:

// 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);

After this code, you can test any public methods on your controller. Of course, if you are testing production code (as I have to; my development code works completely differently because the code base is written terribly) be aware that you might be touching databases, making web calls, etc.

However, the upside is that you can start doing code coverage of your controllers to understand why they're not working properly.

CJ Dennis
  • 4,226
  • 2
  • 40
  • 69