PHPUnitでGeneratorをMockしたい場合ってどうすればよいの？

こんにちは！ @takoba といいます。インターネットと音楽とカレーライスを主にたべるぺちぱーです🍛
現在は [コネヒト株式会社](https://connehito.com/) でサーバーサイドを主戦場にしたエンジニアとして働いています。 CakePHP を主に触っています〜〜🙋‍♀️

このエントリは、 [PHP Advent Calendar 2021](https://qiita.com/advent-calendar/2021/php) の9日目のエントリです。
8日目は @kubotak さんによる [PHPコードを消すライブラリを作った](https://tech.macloud.jp/entry/2021/12/08/000000) でした。


## 前提

### `Generator` is 何？
cf. [PHP: ジェネレータとは \- Manual](https://www.php.net/manual/ja/language.generators.overview.php)
> ジェネレータを使うと、foreach でデータ群を順に処理するコードを書くときに メモリ内で配列を組み立てなくても済むようになります。 メモリ内で配列を組み立てると memory_limit を越えてしまうかもしれないし、 無視できないほどの時間がかかってしまうかもしれません。 配列を作る代わりに、ジェネレータ関数を書くことになります。これは通常の 関数と同じものですが、 ジェネレータ関数は一度だけ return するのではなく、必要に応じて何度でも yield することができます。 つまり、値を繰り返し返せるということです。

要は、 `return` の代わりに `yield` を使うことで、都度、処理の結果を返すことができます。 map 的な処理をする場合に便利そうですね。

例えば、以下のような処理があるとします。

```php
function squareArray(array $arr): array
{
    $res = [];
    foreach ($arr as $v) {
        array_push($res, $v ** 2);
    }

    return $res;
}

foreach (squareArray([1, 2, 3, 4, 5]) as $squared) {
    echo $squared . ',';
}
#=> 1,4,9,16,25,

implode(', ',squareArray([1, 2, 3, 4, 5]))
#=> 1, 4, 9, 16, 25
```

`Generator` を使うと以下のように書けます。
（関数の返り値が `Generator` であることを意識する必要はあるので、ちょっと考えものですが🤔）

```php
function squareGenerator(array $arr): \Generator
{
    foreach ($arr as $v) {
        yield $v ** 2;
    }
}

foreach (squareGenerator([1, 2, 3, 4, 5]) as $squared) {
    echo $squared . ',';
}
#=> 1,4,9,16,25,

implode(', ', iterator_to_array(squareGenerator([1, 2, 3, 4, 5])))
#=> 1, 4, 9, 16, 25
```


### テストしたいコード

今回は、 [jolicode/slack-php-api](https://github.com/jolicode/slack-php-api) を用いて以下のような `class SlackClient` を書いたとします。
`$this->client->iterateUsersList()` が [`iterable`](https://www.php.net/manual/ja/language.types.iterable.php) を返すので、その場合に PHPUnit でどのように書けばよいの？？ってなりました。

```php
<?php
declare(strict_types=1);

namespace App\ExternalService;

use JoliCode\Slack\Api\Model\ObjsUser;
use JoliCode\Slack\Api\Runtime\Client\Client;
use JoliCode\Slack\ClientFactory;

class SlackClient
{

    /**
     * @var \Jolicode\Slack\Api\Runtime\Client\Client Slackクライアント
     */
    private Client $client;

    /**
     * @param \Jolicode\Slack\Api\Runtime\Client\Client|null $client Slackクライアント
     */
    public function __construct(Client $client = null)
    {
        $this->client = $client ?? ClientFactory::create(env('SLACK_TOKEN'));
    }

    /**
     * Slackの表示名からSlackユーザーを取得する
     *
     * @param string $screenName 表示名
     * @return \JoliCode\Slack\Api\Model\ObjsUser Slack側のユーザー情報
     */
    public function getUserByScreenName(string $screenName): ?ObjsUser
    {
        foreach ($this->client->iterateUsersList() as $user) {
            $displayName = $user->getProfile()->getDisplayName();
            if ($displayName === $screenName) {
                return $user;
            }
        }

        return null;
    }
}
```


## 結論

以下のようなかんじに書くとよいです。

```php
/**
 * getUserByScreenName()のテスト
 *
 * @return void
 */
public function test_getUserByScreenName(): void
{
    $screenName = 'dummy';
    $expected = new ObjsUser();
    $expected->setProfile((new ObjsUserProfile())->setDisplayName($screenName));
 
    $clientMock = $this->createPartialMock(Client::class, 'iterateUsersList');
    $clientMock
        ->expects($this->atLeastOnce())
        ->method('iterateUsersList')
        ->will($this->returnCallback(function () use ($expected) {
            foreach ([$expected] as $user) {
                yield $user;
            }
        }));

    $actual = (new SlackClient($clientMock))->getUserByScreenName($screenName);
    $this->assertSame($expected, $actual);
}
```

PHPUnit の `$this->returnCallback()` を用いて `yield $user` として都度 `$user` を返す closure を Mock に突っ込んであげればよいかんじです💪


## おわりに

ナウいかんじの PHP を書くとしたらどう書けばいいんだ〜〜〜？ってたまに迷うことがあったので、 tips として書き起こしてみました。
もし困った時にこのへんが参考になれば、Google先生最高！インターネット幸せ！ってなるのでよきだな〜〜と思っています🙌

明日は @naopusyu さんのエントリです！おたのしみに〜〜👋


## [PR] 最後に宣伝

[コネヒト株式会社](https://connehito.com/) では [ママリ](https://mamari.jp/) を中心に「あなたの家族像が実現できる社会をつくる」ためのプロダクトを鋭意開発しております！
サーバーサイドは CakePHP とか Go で書いていることが多いのですが、このあたりをぼくらと一緒にバリバリ進めてくれるエンジニアを大募集しています〜〜🙏

興味のある方がいらっしゃれば、以下からご応募いただくか、 Twitter で [@takoba_](https://twitter.com/takoba_) までご連絡いただけますと幸いです🤝

- [PHPとAWSでユーザーを支えるサービスをつくるバックエンドエンジニア募集！ \| コネヒト株式会社](https://hrmos.co/pages/connehito/jobs/00b)
- [家族向けサービス運営テックカンパニーをさらにデータドリブンな組織へと導いてくれる、サーバーサイドエンジニア募集！ \| コネヒト株式会社](https://hrmos.co/pages/connehito/jobs/00k)

また、コネヒト社のメンバーが執筆している [コネヒト Advent Calendar 2021](https://qiita.com/advent-calendar/2021/connehito) もぜひチェックしてくれよな！🙏


## 参考
- [PHP: ジェネレータとは \- Manual](https://www.php.net/manual/ja/language.generators.overview.php)
- [Mocking Generators in phpunit — George Papadopoulos' Personal Website —](https://www.gpapadop.gr/blog/2017/11/01/mocking-generators-in-phpunit/)