PHPUnit: Easy Unit Test Writing in Magento 2

PHPUnit: Easy Unit Test Writing in Magento 2

PHPUnit: Easy Unit Test Writing in Magento 2

Having troubles with writing unit tests?


In this blog post I’m going to touch on unit testing in Magento 2 and show you how to write unit tests using PHPUnit framework.


Prepare for writing unit tests


Magento 2 has lots of useful features which go pre-installed, and unit tests are among most important ones. In order to see it in action, we can run tests for the modules that already exist in Magento.

To get started you need to create a configuration file. For the first run, to see how it works, you can run tests for one module.


The configuration file is here:

magento2ce/dev/tests/unit/phpunit.xml.dist


Create a copy of this file and name it

magento2ce/dev/tests/unit/phpunit.xml


Change the paths in it so as you would be able to test a particular module. Let’s take Catalog as an example:  

<testsuite name="Magento Unit Tests">
   <directory suffix="Test.php">../../../app/code/*/Catalog/Test/Unit</directory>
</testsuite>


Now using this configuration you can run a test.

You can do it via console using the phpunit command. It will run tests of phpunit version which is installed in your system. It is recommended that you run the phpunit of the version that is available in Magento 2 repository.

Use the following command:

php vendor/phpunit/phpunit/phpunit -c dev/tests/unit/phpunit.xml

One of the key aspects of writing unit tests is to ensure 100% code coverage. For this we use the following key:

--coverage-html /path – the key for generating report in html format. From my perspective it is the most convenient one of what it’s available out there.

To configure generating of only necessary modules, we change the path as we showed above, only in the directive

<whitelist addUncoveredFilesFromWhiteList="true">

That’s pretty much it. Now you can run tests. Let’s create our own one.

What you need to know about writing unit tests

If you want to quickly learn how to write unit tests, I would recommend you jumped to the middle of the manual and right away started reading about mocks, stubs, etc.

Several times I started to read the manual on writing tests. I saw a bunch of examples that perhaps confuse the beginner, rather than help him.

A typical one:

public function testFailure()
   {
       $this->assertFalse(TRUE);
   }

Let’s check what we get on return with assertFalse(true). Surprise, surprise – we get an error. Or this one

$this->assertArrayHasKey('foo', array('bar' => 'baz'));

Or this

$this->assertEmpty(array('foo'));

It’s no-brainer, right? When you read a manual everything seems clear and no-brainer. But when you want to try your hand at test writing you still don’t know where actually to start.

So, we’ll get to the point.

In writing unit tests, there is such an entity as Mock (or an imitation object). When we create a mock for a class - by default we expect that the methods of this class will return NULL to us.

Thus if we have a class

class Catalog
{
   public function getName()
   {
       return ‘my catalog name’;
   }
}

Moсk of this class for the getName method will return not 'my catalog name', but it will return NULL.

An example of creating a moсk:

$moduleManagerMock = $this->getMock(
   '\Magento\Framework\Module\Manager',
   [],
   [],
   '',
   false
);

1st parameter - the name of the class

2nd - methods that will be replaced by stubs, I’ll tell more about it further

3rd - arguments passed to the constructor

4th - the name of the mock class

5th – if to use the basic constructor

There are more parameters passed to getMock method which you can find in method’s code.

Now, what is a stub?

In fact, a stub is the replacement of results that mock methods return with NULL. When we determine that the A method will return B - that is a stub.

Some peculiarity of mock creation.

Suppose we have a class

class Catalog
{
   public function showA() { echo ‘aaa’; }
   public function showB() { echo ‘bbb’; }
   public function showC() { echo ‘ccc’; }
}

Let’s create a mock for it

$catalogMock = $this->getMock(‘Catalog’, [], [], ‘’, false);

If we call:

$catalogMock->showA();
$catalogMock->showB();
$catalogMock->showC();

We won’t get anything on return since methods are replaced with stubs.

However, if we create such a mock as:

$catMock = $this->getMock(‘Catalog’, [‘showA’, ‘showB’], [], ‘’, false);

And call:

$catMock->showA();
$catMock->showB();
$catMock->showC();

We’ll get the result:

ссс

It happens because we set the stubs only for methods showA and showB!

But if we pass NULL to argument of method list:

$catcatMock = $this->getMock(‘Catalog’, null, [], ‘’, false);

This will mean that we do not define stubs for methods and they will return the result defined in class Catalog.

If we call:

$catcatMock->showA();
$catcatMock->showB();
$catcatMock->showC();

We get:

aaabbbccc

Creating Magento mocks

In Magento 2 there’s a class

\Magento\Framework\TestFramework\Unit\Helper\ObjectManager

This class serves to facilitate the creation of mocks, which already have some functions.

Say you have a Magento collection. Say you want to pass this collection to foreach in a method of work class, so as to walk through them.  

If this collection serves as a common mock, a foreach method will cause a fatal error. It happens since the mock of our collection comes neither as an array nor as an iteratable object.

I highly recommend that you at least look through the methods of the above-mentioned class. They are the most commonly used one when writing tests (in particular the getObject method).

The sequence of test calls

You can find quite detailed information on the sequence of test call in PHPUnit guide, Chapter 4. Fixtures (must-read).

In brief: Most often you’ll have to deal with a setUp() method. This is kind of a constructor which is called before every test run. This is where the mocks are created used for passing a tested class to the constructor. Thus for a class:  

class Aaa
{
    public function setUp() {...}
    public function testMethod1() {...}
    public function testMethod2() {...}
    public function testMethod3() {...}
}

The call will be:

setUp()
testMethod1()
setUp()
testMethod2()
setUp()
testMethod3()

In my case methods tearDown(), setUpBeforeClass() and others are not specified.

Most frequently used patterns

There is nothing extraordinary in the following. To give you a quick example:

Factory Mock:

$productModelFactoryMock = $this->getMock(
   '\Magento\Catalog\Model\ProductFactory',
   ['create'],
   [],
   '',
   false
);

$productMock = $this->getMock('\Magento\Catalog\Model\Product', [], [], '', false);
$productModelFactoryMock->method('create')->willReturn($productMock);

When creating a Factory Mock we need to specify “create” in methods. For me this remains a mystery why it is a necessity, however without it, the method will return Null. Sometimes tests have failed, sometimes not. If you can explain why it works this way – I’ll be reeealy grateful!

Context and what you can create with it

You need to write a test to call a method:

$this->_urlBuilder->getUrl(...);

There’s a builder in _urlBuilder, which is not shown in our class thus we can’t create a stub for it. What shall we do?

Let’s see where urlBuilder is created. We get into an abstract class and in the constructor we see:

$this->_urlBuilder = $context->getUrlBuilder();

So, we do the following:

$this->contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilder);

where contextMock and urlBuilder are the corresponding mocks.

Interface Mocks  

When we create the mock of a class, Magento pass the interface mock to us in a constructor. In other words it’s an object that implements this interface.

In the constructor we have:

\Magento\Store\Model\StoreManagerInterface $storeManager,

But the mock should be created for \Magento\Store\Model\StoreManager

(To define for which class you need to create a mock, check di.xml. There you’ll see your interface and the needed type)

<preference for="Magento\Store\Model\StoreManagerInterface" type="Magento\Store\Model\StoreManager" />

$this->storeManagerMock =
$this->getMock('\Magento\Store\Model\StoreManager', [], [], '', false);

However, there are cases when we need to specifically create a mock of interface, because we have to implement all the methods of this interface in our class (even if it’s a mock). This one doesn’t look good:  

$interfaceMock = $this->getMock('SomeInterface', [‘method1’, ‘method2’, ‘method3’, ‘method4’, ‘method5’, ‘method6’], [], '', false);

Multiple calls to one method

$this->imageHelperMock->expects($this->any()
)->method('init')->withConsecutive(
    [$this->productMock, 'image', $image],
    [$this->productMock, 'thumbnail', $image],
    [$this->productMock, 'small_image', $image],
    [$this->productMock, 'image', $image],
    [$this->productMock, 'thumbnail', $image],
    [$this->productMock, 'small_image', $image]
)->willReturnOnConsecutiveCalls(
    'http://full_path_to_image/magento1.png',
    'http://full_path_to_image/magento1.png',
    'http://full_path_to_image/magento1.png',
    'http://full_path_to_image/magento1.png',
    'http://full_path_to_image/magento1.png',
    'http://full_path_to_image/magento1.png'
);

Such a construction expects 6 calls to the init method with different input parameters. When one method is called several times (!!! here I might be mistaken) it is necessary to specify their correct sequence, otherwise you’ll get an error.

If we expect Exception

public function testLoadWithException()
   {
       $this->setExpectedException('\InputException');
       $this->swatchHelperObject->method(null, ['color' => 31]);
   }

Check if an object is an array

There’s quite a detailed guide on Stack Overflow.

In a nutshell

$this->assertInternalType('array',$students);
$this->assertEquals(7,count($students));
$first=$students[0]; //Previous assert tells us this is safe
$this->assertInstanceOf('Student',$first);

Now, it’s your turn. Write your test units and hopefully the above mentioned information will help you with it. If you have questions, let me know in the comments.




Comments

© Extait, 2019