skip to Main Content

I am using phpunit and want to write unit tests for a class, which uses another class inside (instantiates object of it).
This other class in not passed via constructor or method argument.
For example in javascript you can mock the module, is something similar in phpunit?

The code is as below:

<?php
use SomeLibraryResourceSms;

final class SmsSender
{
    public function __construct(private array $config) {}

    public function send(string $phone, string $content): void
    {
        $sms = new Sms();
        $sms->sendToOne($phone, $content, $this->config['sender']);
    }
}

$sms = new Sms()

Is there a way to mock globally for my unit test the Sms class then stub methods and assert that they are called with the correct arguments

my unit test so far

<?php declare(strict_types = 1);

namespace TestsUnitAppServicesSmsSender;

use AppServicesSmsSender;
use TestsHelpersTestCase;

class SmsSenderTest extends TestCase
{
    private array $config;

    private SmsSender $service;

    public function setUp(): void
    {
        $this->config = [
            'apiKey' => 'mock-api-key',
            'apiSecret' => 'mock-api-secret',
            'sender' => 'my-company-name'
        ];

        $this->service = new SmsSender(
            $this->sms->reveal(),
        );
    }

    public function testSend(): void
    {
        // how to mock the SomeLibraryResourceSms ?
        $this->service->send('+3344334', 'dfddfdfdf');
    }
}

UPDATE:
I used one of the provided answers here and it works fine, re-declaring the class in my unit test file and using a custom mock.

**namespace SomeLibraryResourceSms;
class Sms {
    public static array $calls = [];

    public function sendToOne(string $phone, string $content, string $sender): void {
        if (!array_key_exists('sendToOne', self::$calls)) {
            self::$calls['sendToOne'] = [];
        }
        self::$calls['sendToOne'][] = func_get_args();
    }
}

then using the custom mock asserted like

self::assertEquals([$phoneNumber, $content, $this->config['sender']], Sms::$calls['sendToOne'][0]);

3

Answers


  1. Chosen as BEST ANSWER

    Thanks to @MadCatERZ answer I managed to have my unit test working by re-declaring the Sms class inside the unit test and using a custom mock. It is very hard to stub yourself though, easy to see the calls and arguments only..

    my new unit test file looks like

    <?php declare(strict_types = 1);
    
    namespace TestsUnitAppServicesSmsSender;
    
    use AppServicesSmsSender;
    use TestsHelpersTestCase;
    
    class SmsSenderTest extends TestCase
    {
        private array $config;
    
        private SmsSender $service;
    
        public function setUp(): void
        {
            $this->config = [
                'apiKey' => 'mock-api-key',
                'apiSecret' => 'mock-api-secret',
                'sender' => 'my-company-name'
            ];
    
            $this->service = new SmsSender(
                $this->sms->reveal(),
            );
        }
    
        public function testSend(): void
        {
            $phoneNumber = '+3344334';
            $content = 'dfddfdfdf';
            $this->service->send($phoneNumber, $content);
            self::assertEquals([$phoneNumber, $content, $this->config['sender']], Sms::$calls['sendToOne'][0]);
        }
    }
    
    namespace SomeLibraryResourceSms;
    class Sms {
        public static array $calls = [];
    
        public function sendToOne(string $phone, string $content, string $sender): void {
            if (!array_key_exists('sendToOne', self::$calls)) {
                self::$calls['sendToOne'] = [];
            }
            self::$calls['sendToOne'][] = func_get_args();
        }
    }
    

  2. Try to add an Sms-Stub to your Unit test

    <?php declare(strict_types = 1);
    
    namespace TestsUnitAppServicesSmsSender;
    
    use AppServicesSmsSender;
    use TestsHelpersTestCase;
    
    class Sms {...
    

    As your script runs in namespace TestsUnitAppServicesSmsSender, the PHP-Interpreter will first take a look under this Namespace for the Sms-Class

    Login or Signup to reply.
  3. I also implemented successfully one alternative solution proposed by @m-eriksson
    It involves using the Factory pattern.
    As I did not want to have logic inside the DI container and slow down all calls with runtime code there, I used a factory as second argument of the SmsSender
    Then mocked the response.

    final class SmsSender
    {
        public function __construct(private array $config, private SmsGlobalSmsFactory $smsFactory)
        {
            Credentials::set($this->config['apiKey'], $this->config['apiSecret']);
        }
    
        public function send(string $phone, string $content): void
        {
            $sms = $this->smsFactory->create($this->config); // this is the change
            $sms->sendToOne($phone, $content, $this->config['sender']);
        }
    }
    

    Then in the unit tests had more flexibility

        $this->service = new SmsGlobalSmsSender(
                $this->config,
                $this->smsFactory->reveal(),
            );
    

    and could mock the response of smsFactory to use a mock and assert all calls and mock the responses.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search