Update cookies preferences

Writing a Unit Test for Magento 2

How to write a unit test in Magento 2, for those with prior knowledge of PHPUnit Software.

This article is for those with prior knowledge of PHPUnit software and will cover 'How to Write a Unit Test in Magento 2' using a real-world example. The basics, definitions and benefits of PHPUnit are scoped out, these are well-documented topics and we will focus mainly on writing a practical PHPUnit test with Magento 2. 

Unit testing can help ensure your development and code are of the highest quality, helping avoid problems in advance and therefore saving your project time and money.

Real life unit test:

For a given customer module i.e app/code/Vendor/ModuleToTest, our unit tests will reside inside the Test/Unit folder based on naming standards. Often they end up with a folder structure that's similar to a Magento module, with all the testable components in their corresponding folders:



Creating a Unit Test File

We will be writing a unit test for the Scan controller within our custom module:
app/code/Develodesign/BarcodeScanner/Adminhtml/Barcodescanner/Scan.php  

Below is the content of the class we will be testing; All it does really is get the order id from the request param and passes it to the order repository, which in turns returns a matching order if it exists. 

<?php

namespace Develodesign\BarcodeScanner\Controller\Adminhtml\Barcodescanner;

use Magento\Framework\App\Action\HttpGetActionInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\Controller\Result\Json;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\Controller\ResultInterface;
use Magento\Framework\Exception\LocalizedException;
use Magento\Sales\Api\OrderRepositoryInterface;

class Scan implements HttpGetActionInterface
{
    /**
     * @var RequestInterface
     */
    protected $request;

    /**
     * @var OrderRepositoryInterface
     */
    protected $orderRepository;

    /**
     * @var Json
     */
    protected $result;

    /**
     * Constructor
     *
     * @param RequestInterface         $request
     * @param OrderRepositoryInterface $orderRepository
     * @param JsonFactory              $resultJsonFactory
     */
    public function __construct(
        RequestInterface         $request,
        OrderRepositoryInterface $orderRepository,
        JsonFactory              $resultJsonFactory
    ) {
        $this->request = $request;
        $this->orderRepository = $orderRepository;
        $this->result = $resultJsonFactory->create();
    }

    /**
     * @return ResponseInterface|Json|ResultInterface
     * @throws LocalizedException
     */
    public function execute()
    {
        $orderId = (int)$this->request->getParam('order_id');
        if (!$orderId) {
            throw new LocalizedException(
                new \Magento\Framework\Phrase('Query parameter, Order ID is required to perform this action')
            );
        }

        $order = $this->orderRepository->get($orderId);

        if ($order) {
            $this->result->setData([
                'success'  => true,
                'error'    => false,
                'response' => $order->getData()
            ]);
        }

        return $this->result;
    }
}


We will be creating a corresponding test class ScanTest,  therefore the whole path will be:
app/code/Develodesign/BarcodeScanner/Test/Unit/Controller/Adminhtml/ScanTest.php 


Adding Modifiers, Matching Constructor Signature and Instantiating Class in our Test Class

We will start off by overriding the set-up method, this being the method called before each test. Within this method, we will match the modifiers, and constructor signature, and instantiate the class which we are testing. We will end up with this:

Modifiers/Properties

Matching the modifiers in the main class i.e Scan.php  

namespace Develodesign\BarcodeScanner\Test\Unit\Controller\Adminhtml;
use Magento\Framework\Controller\Result\JsonFactory;
use Magento\Framework\App\RequestInterface; use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Sales\Api\Data\OrderInterface;
use Magento\Sales\Api\OrderRepositoryInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Develodesign\Barcodecanner\Controller\Adminhtml\Barcodescanner\Scan;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\Controller\Result\Json;

class Scan implements TestCase
{
/** * @var Scan */
private $controller; /** * @var RequestInterface|MockObject; */ private $requestInterfaceMock; /** * @var OrderRepositoryInterface|MockObject */ private $orderRepositoryMock; /** * @var Json|MockObject */ private $resultJsonMock; /** * @var JsonFactory|MockObject */ private $resultJsonFactoryMock;


Matching Constructor Signature and class instantiation

Instantiating our main class and matching its constructor signature. 

protected function setUp(): void
{
    $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class)
        ->disableOriginalConstructor()
        ->getMockForAbstractClass();

    $this->requestInterfaceMock = $this->getMockBuilder(RequestInterface::class)
        ->disableOriginalConstructor()
        ->getMockForAbstractClass();

    $this->resultJsonFactoryMock = $this->createMock(JsonFactory::class);

    $this->resultJsonMock = $this->createMock(Json::class);

    $this->resultJsonFactoryMock->expects($this->once())
        ->method('create')
        ->willReturn($this->resultJsonMock);

    $this->controller = new Scan(
        $this->requestInterfaceMock,
        $this->orderRepositoryMock,
        $this->resultJsonFactoryMock
    );
}


Writing a Unit Test:

We can now proceed to write test cases for the execute method within our Scan class. The Order Id (order_id) is a required parameter from the request and as such an exception is thrown when it's not set. We will write a test case "testExecuteNullRequestParam()” which will cover this base as shown below:

/**
 * @return void
 * @throws LocalizedException
 */
public function testExecuteNullRequestParam(): void
{
    $orderId = null;
    $param = 'order_id';
    $this->requestInterfaceMock->expects($this->once())->method('getParam')->with($param)->willReturn($orderId);
    $this->expectException(LocalizedException::class);
    $this->expectExceptionMessage('Query parameter, Order ID is required to perform this action');
    $this->controller->execute();
}


PHPUnit enables us to use its various testing functionalities such as the expects method which accepts an invocation order i.e method invoked once or any. We can specify arguments using with($param) and return value will Return($orderId). Furthermore, as our class is extending the PHPUnit, it allows us to make method calls such as expectException and expectExceptionMessage. We can define multiple test methods in the same test model. Here is the full file with other test cases


Running a Unit Test

We can run this single unit test via the CLI php: 

vendor/phpunit/phpunit/phpunit -c dev/tests/unit/phpunit.xml.dist app/code/Develodesign/BarcodeScanner/Test/Unit/Controller/Adminhtml/ScanTest.php 



Incorporating Unit testing into your development process can help fix bugs throughout the Magento web development, preventing the delay of launches.

Do you need support from our Magento Certified Professionals?


...
Raphael

share:

Ready to start your next project?

See how we can help you accelerate your business