Unit testing in Magento 2

Related Inchoo Services

Magento 2 comes pre-installed with PHPUnit, an automated testing framework for PHP. It is included as one of the dependencies in Magento 2. Covering the basics of PHPUnit is out of the scope of this tutorial, after a short introduction we are going to focus on the practical example of using PHPUnit with Magento 2. For those who are interested in PHPUnit basics, I would recommend reading documentation or tutorials on the web since it is a very well documented topic.

What is Unit Testing?

Unit testing is a level of software testing where individual units/ components of a software are tested. The purpose is to validate that each unit of the software performs as designed. A unit is the smallest testable part of any software. It usually has one or a few inputs and usually a single output. Unit testing process is separate and completely automatic without any manual handling.

Why Unit Test?

Every once in a while in development world the same question appears. Why would I unit test my code? Is it worth the effort?

So what would actually be the benefits of unit testing?

  • Find problems early

Unit testing finds problems early in the development cycle. This includes both bugs in the programmer’s implementation and flaws or missing parts of the specification for the unit. The process of writing a thorough set of tests forces the author to think through inputs, outputs, and error conditions, and thus more crisply define the unit’s desired behavior

  • Facilitates change

Unit testing allows the programmer to refactor code or upgrade system libraries at a later date, and make sure the module still works correctly. The procedure is to write test cases for all functions and methods so that whenever a change causes a fault, it can be quickly identified.

  •  Design

Writing the test first forces you to think through your design and what it must accomplish before you write the code. This not only keeps you focused; it makes you create better designs. Testing a piece of code forces you to define what that code is responsible for. If you can do this easily, that means the code’s responsibility is well-defined and therefore that it has high cohesion.

Writting simple unit test in Magento 2

Given that we have custom module named Testing under Inchoo namespace (app/code/Inchoo/Testing), our unit tests will reside inside Test/Unit folder according to naming standards.

Class that we are going to test will be called SampleClass and will reside in TestingClass folder so the whole path would be – app/code/Inchoo/Testing/TestingClass/SampleClass.
Our class code looks like this:

<?php
namespace Inchoo\Testing\TestingClass;
 
class SampleClass
{
    public function getMessage()
    {
        return 'Hello, this is sample test';
    }
}
?>

As you can see this class is pretty straightforward, it just returns simple string.
Our test that will test getMessage() method will look like this:

<?php
 
namespace Inchoo\Testing\Test\Unit;
 
use Inchoo\Testing\TestingClass\SampleClass;
 
class SampleTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @var Inchoo\Testing\TestingClass\SampleClass
     */
    protected $sampleClass;
 
    /**
     * @var string
     */
    protected $expectedMessage;
 
    public function setUp()
    {
        $objectManager = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this);
        $this->sampleClass = $objectManager->getObject('Inchoo\Testing\TestingClass\SampleClass');
        $this->expectedMessage = 'Hello, this is sample test';
    }
 
    public function testGetMessage()
    {
        $this->assertEquals($this->expectedMessage, $this->sampleClass->getMessage());
    }
 
}

As you can see this unit test just tests output of the getMessage() method, for test to pass it should be equal to expectedMessage property in the test. Our test extends PHPUnit which gives us assertEquals() method along with many more testing functionalities. There is also setUp() method that is run before testing methods inside of a test, it sets up testing environment along with every class or property that we will need in our test.

Running unit tests in PhpStorm

There are multiple ways to run PHPUnit test:

  • CLI
  • Magento Commands
  • IDE integration (PHPStorm)

In this section we will describe how to run Unit tests over PHPStorm.

To run unit tests over PhpStorm, IDE needs to be configured first. There is PHPUnit configuration file shipped with Magento 2 – {ROOT}/dev/tests/unit/phpunit.xml.dist

Configuration file tells PHPUnit where to find test cases to run (along with other information). To make it active copy file in the same folder and remove .dist extension, so the name is now phpunit.xml.

In the file under the section, we will need to add our test location. In this example, other tests locations are commented out because we don’t want PHPUnit to run all the tests, just the one we have written earlier. So our phpunit.xml should look like this:

<testsuite name="Magento Unit Tests">
    <directory suffix="Test.php">../../../app/code/Inchoo/Testing/Test/Unit</directory>
    <!--<directory suffix="Test.php">../../../lib/internal/*/*/Test/Unit</directory>-->
    <!--<directory suffix="Test.php">../../../lib/internal/*/*/*/Test/Unit</directory>-->
    <!--<directory suffix="Test.php">../../../setup/src/*/*/Test/Unit</directory>-->
    <!--<directory suffix="Test.php">../../../vendor/*/module-*/Test/Unit</directory>-->
    <!--<directory suffix="Test.php">../../../vendor/*/framework/Test/Unit</directory>-->
    <!--<directory suffix="Test.php">../../../vendor/*/framework/*/Test/Unit</directory>-->
    <!--<directory suffix="Test.php">./*/*/Test/Unit</directory>-->
</testsuite>

Now PHPUnit can find our test. To configure PHPStorm to be able to run the tests we need to go to Toolbar > Run > Edit Configurations. After popup window appears we click on plus sign to add new configuration.

From the drop-down we choose PHPUnit and under Test Runner section we choose Define in the configuration file and check the Use alternative configuration file and finally select phpunit.xml file we edited earlier.

Now PHPStorm knows about our tests and is able to find them. There is just one more step to complete configuring. PHPStorm should also know about PHPUnit library location and interpreter location. To configure this we should go to File > Settings > Languages & Frameworks > PHP > Test Frameworks. Inside this section, we want to add new configuration using + symbol.

Under the assumption that we use Composer autoloader we would choose Use composer autoloader option and select vendor/autoload.php. Composer autoloader knows the location of all classes so connecting PHPStorm to autoload.php will finish configuration of PHPStorm successfully.

To run our test we can click on the Run icon next to the configuration options in the upper right corner of PHPStorm.

Test should pass and our console looks like this:

Analysis of Magento test

Here we have PostDataProcessorTest from CMS module. And we are going to break it down piece by piece. This test tests validateRequireEntry() method from PostDataProcessor class. This method validates input by checking if required fields are empty. If some of the fields are empty the whole request sequence is stopped with an error.

<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Cms\Test\Unit\Controller\Page;
 
use Magento\Cms\Controller\Adminhtml\Page\PostDataProcessor;
use Magento\Framework\Stdlib\DateTime\Filter\Date;
use Magento\Framework\Message\ManagerInterface;
use Magento\Framework\TestFramework\Unit\Helper\ObjectManager;
use Magento\Framework\View\Model\Layout\Update\ValidatorFactory;
 
/**
 * Class PostDataProcessorTest
 * @package Magento\Cms\Test\Unit\Controller\Page
 */
class PostDataProcessorTest extends \PHPUnit\Framework\TestCase
{
    /**
     * @var Date|\PHPUnit_Framework_MockObject_MockObject
     */
    protected $dateFilterMock;
 
    /**
     * @var ManagerInterface|\PHPUnit_Framework_MockObject_MockObject
     */
    protected $messageManagerMock;
 
    /**
     * @var ValidatorFactory|\PHPUnit_Framework_MockObject_MockObject
     */
    protected $validatorFactoryMock;
 
    /**
     * @var PostDataProcessor
     */
    protected $postDataProcessor;
 
    protected function setUp()
    {
        $this->dateFilterMock = $this->getMockBuilder(Date::class)
            ->disableOriginalConstructor()
            ->getMock();
        $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class)
            ->getMockForAbstractClass();
        $this->validatorFactoryMock = $this->getMockBuilder(ValidatorFactory::class)
            ->disableOriginalConstructor()
            ->setMethods(['create'])
            ->getMock();
 
        $this->postDataProcessor = (new ObjectManager($this))->getObject(
            PostDataProcessor::class,
            [
                'dateFilter' => $this->dateFilterMock,
                'messageManager' => $this->messageManagerMock,
                'validatorFactory' => $this->validatorFactoryMock
            ]
        );
    }
 
    public function testValidateRequireEntry()
    {
        $postData = [
            'title' => ''
        ];
        $this->messageManagerMock->expects($this->once())
            ->method('addError')
            ->with(__('To apply changes you should fill in hidden required "%1" field', 'Page Title'));
 
        $this->assertFalse($this->postDataProcessor->validateRequireEntry($postData));
    }
}

First thing to notice is the setUp method, when running tests it is the first test method that is run. Purpose of the setUp method is to set the testing environment – instantiate objects/mocks, populate properties…

In this example setUp() method is creating mocks using MockBuilder and populating test properties with created mocks.

There is also PostDataProcessor instantiation, which is logical because testing that class is the whole purpose of this test.

PostDataProcessor class requires Date, ManagerInterface and ValidatorFactory dependencies in the constructor, that is why there are mocks of those classes created. Mocks of concrete classes are passed to PostDataProcessor constructor which doesn’t complain since mock is an imitation of real class.

Our test class testValidateRequireEntry simulates case in which parameter array with empty title parameter is sent to validation.

There are two assertions:

  • messageManagerMock will be called just once, method that will be called is addError() method, which will return ‘To apply changes you should fill in hidden required title field’. Since this is just a mock and not real class there must be developer defined method calls and responses.
  • validateRequireEntry() method will return false, because it will try to validate parameters array with empty field and that will cause error in validation and return false.

Both assertions will pass in this case.

Conclusion

In this article, we have just scratched the surface of Magento 2 unit testing and unit testing in general. This topic is very complex and it is not possible to cover a lot in just one article. So I encourage you to read more about it either from PHPUnit documentation or from Magento 2 specific testing tutorials. Best Magento 2 testing examples are located in Magento core naturally. Happy testing.

You made it all the way down here so you must have enjoyed this post! You may also like:

Development environment for Magento 2 using Docker Tomas Novoselic
, | 0

Development environment for Magento 2 using Docker

Contribution Day at MM18HR – work side by side with Magento Community Engineering team Maja Kardum
, | 0

Contribution Day at MM18HR – work side by side with Magento Community Engineering team

How to add a custom columns to the Magento 2 products grid Ivan Matozan
, | 1

How to add a custom columns to the Magento 2 products grid

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <blockquote cite=""> <code> <del datetime=""> <em> <s> <strike> <strong>. You may use following syntax for source code: <pre><code>$current = "Inchoo";</code></pre>.