Vinai Kopp

Magento Expert, Developer & Trainer

  • 07. The Action Controller Integration Test Kata

    April 18, 2016

    Mage2Katas

    This kata is (again) about creating a new Action Controller.
    In contrast to the last kata however we will be writing integration tests to verify our controller works correctly in the context of Magento 2.

    I’v written before that action controllers mainly are entry points into the application.
    They should get arguments from the request and return the required result, and maybe do some basic error handling, but they usually should not contain any real business logic.
    I consider them part of the UI layer of the application.
    So in this tests we mainly want to test they delegate to the right collaborators and send the correct response.

    So what should our new action do?

    For the purpose of this kata, lets say we want to render a HTML page for GET requests, while POST requests should display a 404 Not Found page.

    Note: Technically the response should use the 405 Method Not Allowed HTTP response code, but since that is not used commonly, lets settle for a pragmatic, well known 404 instead for this kata.

    The module name will be Mage2Kata_ActionController.
    I already created the module by following the steps of the module config kata and the route config kata.

    We can assume that the route is correctly configured, and that a skeleton action controller class already exists.

    Time to add functionality to our new action. For that lets create a dedicated test case in the file
    Test/Integration/Controller/Index/IndexIntegrationTest.php.

    This test extends a special abstract controller test case class provided by the Magento 2 integration test framework:
    Magento\TestFramework\TestCase\AbstractController.

    <?php
     
    namespace Mage2Kata\ActionController\Controller\Index;
     
    use Magento\TestFramework\Request;
    use Magento\TestFramework\TestCase\AbstractController as ControllerTestCase;
     
    class IndexIntegrationTest extends ControllerTestCase
    {
        public function testCanHandleGetRequests()
        {
            $this->getRequest()->setMethod(Request::METHOD_GET);
            $this->dispatch('mage2kata/index/index');
            $this->assertSame(200, $this->getResponse()->getHttpResponseCode());
        }
    }

    The AbstractController test case allows us to call the dispatch() method, simulating a real request.

    We also set the request method to GET even though that is the default anyway, simply for documentary value.

    Unfortunately, the test already passes.
    Seems like the HTTP 200 response code is the default, as long as the request was routed successfully.

    Also note that the abstract controller test case automatically took care of setting the application scope to frontend. We did not have to add a @magentoAppArea annotation to the test ourselves.

    Lets add a check that an actual HTML page was returned.

    public function testCanHandleGetRequests()
    {
        $this->getRequest()->setMethod(Request::METHOD_GET);
        $this->dispatch('mage2kata/index/index');
     
        $this->assertSame(200, $this->getResponse()->getHttpResponseCode());
        $this->assertRegExp('#<body [^>]+>#s', $this->getResponse()->getBody());
    }

    Note the extra line checking the response body contains a <body> tag.
    In a real work scenario, the test could check for more specific page content of course.
    Probably using a regular expression here is overkill, simply using assertContains() would have been good enough.

    But either way, since the response body is empty, the test fails.

    To make it pass we need to use a Result\PageFactory to create a Page result so we can return it from the execute() method.

    <?php
     
    namespace Mage2Kata\ActionController\Controller\Index;
     
    use Magento\Framework\App\Action\Action;
    use Magento\Framework\App\Action\Context;
    use Magento\Framework\View\Result\PageFactory as PageResultFactory;
     
    class Index extends Action
    {
        /**
         * @var PageResultFactory
         */
        private $pageFactory;
     
        public function __construct(Context $context, PageResultFactory $pageFactory)
        {
            parent::__construct($context);
            $this->pageFactory = $pageFactory;
        }
     
        public function execute()
        {
            return $this->pageFactory->create();
        }
    }

    The rendered page still looks empty in the browser, but it actually is not empty any more: the root template is rendered, which contains a <body> tag, so our test passes! The page just looks empty because the HTML page body has no content.

    To create the page content we would need to add layout XML, but that will be part of another episode. This episode focuses on the controller logic.

    Okay, then what next?

    Lets take care of routing POST requests to the 404 noroute action controller, starting with a test for that.

    public function testCanNotHandlePostRequests()
    {
        $this->getRequest()->setMethod(Request::METHOD_POST);
        $this->dispatch('mage2kata/index/index');
     
        $this->assertSame(404, $this->getResponse()->getHttpResponseCode());
    }

    Note: the abstract controller test case class also provides a assert404NotFound() method, but that doesn’t take into account the HTTP response code. But we can add that to our test to be extra sure we really are returning a 404 page.

    The new test fails, since we are currently always returning a Page result.

    To make it pass, we need to add a new Magento\Framework\Controller\Result\ForwardFactory constructor argument, and use that to forward visitors to the noroute action controller if the request method is POST.

    <?php
     
    namespace Mage2Kata\ActionController\Controller\Index;
     
    use Magento\Framework\App\Action\Action;
    use Magento\Framework\App\Action\Context;
    use Magento\Framework\Controller\Result\ForwardFactory as ForwardResultFactory;
    use Magento\Framework\View\Result\PageFactory as PageResultFactory;
     
    class Index extends Action
    {
        /**
         * @var PageResultFactory
         */
        private $pageFactory;
     
        /**
         * @var ForwardResultFactory
         */
        private $forwardFactory;
     
        public function __construct(Context $context, PageResultFactory $pageFactory, ForwardResultFactory $forwardFactory)
        {
            parent::__construct($context);
            $this->pageFactory = $pageFactory;
            $this->forwardFactory = $forwardFactory;
        }
     
        public function execute()
        {
            if ($this->getRequest()->getMethod() === 'POST') {
                $forward = $this->forwardFactory->create();
                $forward->forward('noroute');
                return $forward;
            }
            return $this->pageFactory->create();
        }
    }

    And this turns our test green again.

    What’s left is just a bit of refactoring of the action controller. While doing so the tests keep us from breaking anything.

    <?php
     
    namespace Mage2Kata\ActionController\Controller\Index;
     
    use Magento\Framework\App\Action\Action;
    use Magento\Framework\App\Action\Context;
    use Magento\Framework\Controller\Result\ForwardFactory as ForwardResultFactory;
    use Magento\Framework\View\Result\PageFactory as PageResultFactory;
     
    class Index extends Action
    {
        /**
         * @var PageResultFactory
         */
        private $pageFactory;
     
        /**
         * @var ForwardResultFactory
         */
        private $forwardFactory;
     
        public function __construct(Context $context, PageResultFactory $pageFactory, ForwardResultFactory $forwardFactory)
        {
            parent::__construct($context);
            $this->pageFactory = $pageFactory;
            $this->forwardFactory = $forwardFactory;
        }
     
        public function execute()
        {
            return $this->isPostRequest() ?
                $this->handlePostRequest() :
                $this->handleGetRequest();
        }
     
        /**
         * @return bool
         */
        private function isPostRequest()
        {
            return $this->getRequest()->getMethod() === 'POST';
        }
     
        /**
         * @return \Magento\Framework\App\Request\Http
         */
        public function getRequest()
        {
            return parent::getRequest();
        }
     
        /**
         * @return \Magento\Framework\Controller\Result\Forward
         */
        private function handlePostRequest()
        {
            $forward = $this->forwardFactory->create();
            $forward->forward('noroute');
            return $forward;
        }
     
        /**
         * @return \Magento\Framework\View\Result\Page
         */
        private function handleGetRequest()
        {
            return $this->pageFactory->create();
        }
    }

    The getRequest() method is overridden only to up-cast the return value from
    \Magento\Framework\App\RequestInterface to
    \Magento\Framework\App\Request\Http
    in the PHPDoc type hint, as getMethod() unfortunately is not part of the RequestInterface definition.

    And with that the Action Controller Mage2Kata is complete.

    I hope you enjoyed it!

    As always, please let me know if you have any feedback, either as a comment below the video on youtube, or on twitter.

    Thanks for following along!

    comments powered by Disqus