如何在PHP中模拟一个未通过构造函数或方法参数传递的类?(phpunit)

huangapple go评论66阅读模式
英文:

How to mock a class in php that is not passed via constructor or method argument ? (phpunit)

问题

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 SomeLibrary\Resource\Sms;

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 Tests\Unit\App\Services\SmsSender;

use App\Services\SmsSender;
use Tests\Helpers\TestCase;

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 SomeLibrary\Resource\Sms ?
        $this->service->send('+3344334', 'dfddfdfdf');
    }
}

UPDATE:

I got two great suggestions from you and posted the implemented solutions from me here, while giving credit to the person that responded.

英文:

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:

&lt;?php
use SomeLibrary\Resource\Sms;

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

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

$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

&lt;?php declare(strict_types = 1);

namespace Tests\Unit\App\Services\SmsSender;

use App\Services\SmsSender;
use Tests\Helpers\TestCase;

class SmsSenderTest extends TestCase
{
    private array $config;

    private SmsSender $service;

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

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

    public function testSend(): void
    {
        // how to mock the SomeLibrary\Resource\Sms ?
        $this-&gt;service-&gt;send(&#39;+3344334&#39;, &#39;dfddfdfdf&#39;);
    }
}

UPDATE:

I got two great suggestions from you and posted the implemented solutions from me here, while giving credit to the person that responed.

答案1

得分: 0

尝试将一个Sms-Stub添加到您的单元测试中

<?php declare(strict_types = 1);

namespace Tests\Unit\App\Services\SmsSender;

use App\Services\SmsSender;
use Tests\Helpers\TestCase;

class Sms {....

由于您的脚本运行在Tests\Unit\App\Services\SmsSender命名空间中,PHP解释器将首先在此命名空间下查找Sms类。

英文:

Try to add an Sms-Stub to your Unit test

&lt;?php declare(strict_types = 1);

namespace Tests\Unit\App\Services\SmsSender;

use App\Services\SmsSender;
use Tests\Helpers\TestCase;

class Sms {...

As your script runs in namespace Tests\Unit\App\Services\SmsSender, the PHP-Interpreter will first take a look under this Namespace for the Sms-Class

答案2

得分: 0

以下是翻译好的部分:

解决方案 1 - 使用手动模拟存根

感谢 @MadCatERZ 的回答,我通过在单元测试中重新声明 Sms 类并使用自定义模拟成功使我的单元测试工作起来。尽管很难自己存根,但只能看到调用和参数很容易。

我的新单元测试文件如下:

<?php declare(strict_types = 1);

namespace Tests\Unit\App\Services\SmsSender;

use App\Services\SmsSender;
use Tests\Helpers\TestCase;

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 SomeLibrary\Resource\Sms;
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();
    }
}

请注意,代码中的 HTML 实体编码已被移除,以便更好地呈现代码。

英文:

Solution 1 - using manual mock-stub

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

&lt;?php declare(strict_types = 1);

namespace Tests\Unit\App\Services\SmsSender;

use App\Services\SmsSender;
use Tests\Helpers\TestCase;

class SmsSenderTest extends TestCase
{
    private array $config;

    private SmsSender $service;

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

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

    public function testSend(): void
    {
        $phoneNumber = &#39;+3344334&#39;;
        $content = &#39;dfddfdfdf&#39;;
        $this-&gt;service-&gt;send($phoneNumber, $content);
        self::assertEquals([$phoneNumber, $content, $this-&gt;config[&#39;sender&#39;]], Sms::$calls[&#39;sendToOne&#39;][0]);
    }
}

namespace SomeLibrary\Resource\Sms;
class Sms {
    public static array $calls = [];

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

答案3

得分: 0

**解决方案 2 - 使用工厂模式和 DI 注入**

我还成功实现了 @m-eriksson 提出的另一种替代方案。
它涉及使用`工厂`模式。
由于我不想在 DI 容器中放置逻辑并使所有调用都受到运行时代码的影响,我将工厂作为`SmsSender`的第二个参数使用。
然后模拟了响应。

```php
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); // 这是更改的部分
        $sms->sendToOne($phone, $content, $this->config['sender']);
    }
}

然后在单元测试中具有更多的灵活性

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

并且可以模拟 smsFactory 的响应以使用模拟并断言所有调用和模拟响应。


<details>
<summary>英文:</summary>

**Solution 2 - using factory pattern and di injection**

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-&gt;smsFactory-&gt;create($this-&gt;config); // this is the change
    $sms-&gt;sendToOne($phone, $content, $this-&gt;config[&#39;sender&#39;]);
}

}



Then in the unit tests had more flexibility 

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

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



</details>



# 答案4
**得分**: 0

如果您已经在测试`Sms::sendToOne()`方法,那么无需再测试`SmsSender::send()`。在您的情况下,`send()`只是`sendToOne()`的包装器,并没有执行任何额外的逻辑。

如果您在`SmsSender::send()`中添加更多代码,那么很遗憾地告诉您,您无法模拟`Sms`类。

<details>
<summary>英文:</summary>

If you are already testing `Sms::sendToOne()` method there is no need to test `SmsSender::send()` as well. In your case `send()` is just a wrapper over `sendToOne()` and doesn&#39;t do any extra logic.

If you were to add more code in `SmsSender::send()` then I am sad to say you cannot mock the `Sms` class.

</details>



huangapple
  • 本文由 发表于 2023年6月19日 17:00:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/76505125.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定