PHPUnit 配列の順番は考慮・気にしないAssert

## 概要

配列（連想配列も含む）の順番は気にせず、要素は同じであることを検証するためのAssert

※ そもそも、順番も保証された上でのテストであったほうが良いというのも分かるのですが、今回はスコープ外とします

※ こんなAssertがあればいいなと作成したものなので「既にPHPUnitで用意されてるよ」などあればコメントお願いします🙏

## 本題

利用したいテストでこのTraitを使う想定でこんな感じで実装しました :pencil: 

```php
<?php

trait ArraySimilarTrait
{
    /**
     * PHPUnit assert arrays ignore orders.
     *
     * @param array $expected
     * @param array $actual
     */
    protected function assertArraySimilar(array $expected, array $actual)
    {
        $this->assertSame([], array_diff_key($actual, $expected));

        array_multisort($expected);
        array_multisort($actual);

        foreach ($expected as $key => $value) {
            if (is_array($value)) {
                $this->assertArraySimilar($value, $actual[$key]);
            } else {
                $this->assertContains($value, $actual);
            }
        }
    }
}
```

[gist](https://gist.github.com/akkiihs/4d7298bfee9faca12c967e911005b811)でも公開しています

ポイントとしては以下の3つです。

1. 比較する2つの配列でキーが同じであること
2. 配列・連想配列どちらでも対応できること
3. どの次元においても順番は異なっても問題ないこと

### 1. 比較する2つの配列でキーが同じであること

```php
$this->assertSame([], array_diff_key($actual, $expected));
```

言葉の通りですが、比較する2つの配列で同じキーで差分がないことを検証しています。

### 2. 配列・連想配列どちらでも対応できること

```php
array_multisort($expected);
array_multisort($actual);
```

「順番を考慮しないはずなのに、なぜ並び替えをしているのか。」という疑問が生まれると思います。

今回、配列のどの次元においても順番は気にない仕様にしております。

そのために、要素が配列であれば、続くコードで以下のように再帰的に `assertArraySimilar` メソッドを呼び出すようにしています。

```php
foreach ($expected as $key => $value) {
  if (is_array($value)) {
    $this->assertArraySimilar($value, $actual[$key]); // 👈 配列（Array）の場合、添字での参照になります
  } else {
    $this->assertContains($value, $actual);
  }
}
```

コメントにも書いたように `$actual[$key]` のように書くと、配列（Array）の場合、添字での参照になります。

そのため、配列（Array）の場合でも、同じ要素へ参照するために並び替えをしています。

※ ここで `配列（Array）` という書き方をしたのは、 `連想配列（Hash）` と区別するためです。 

PHPには、配列（Array）, 連想配列（Hash）は型として区別されていませんが、 `array_multisort` メソッドを利用することで、
配列（Array）であれば、キーを振り直す `sort` メソッドにあたる挙動を
連想配列（Hash）であれば、キーを振り直さない `asort` メソッドにあたる挙動をしてくれます。

### 3. どの次元においても順番は異なっても問題ないこと

`2. 配列・連想配列どちらでも対応できること` でほぼ書いてしまいましたが、
再帰的に `assertArraySimilar` メソッドを呼び出すことで、
どの次元においても順番は異なっても気にない = Pass します。

---

### テスト用Traitのテスト

今回、作成したAssertが期待した挙動になっているのか確認のためテストも書きました。

どういうパターンだと成功・失敗するのかこちらのほうがわかりやすいかと思います。

```php
<?php

class ArraySimilarTraitTest extends \PHPUnit\Framework\TestCase
{
    use ArraySimilarTrait;

    /**
     * @test
     * @dataProvider data_assertArraySimilar_expected_passed
     * @param array $expected
     * @param array $actual
     */
    public function assertArraySimilar_expected_passed(array $expected, array $actual)
    {
        $this->assertArraySimilar($expected, $actual);
    }

    public function data_assertArraySimilar_expected_passed()
    {
        return [
            '1D Array' => [
                [1, 2, 3],
                [3, 2, 1],
            ],
            '1D Hash' => [
                ['a' => 1, 'b' => 2, 'c' => 3],
                ['c' => 3, 'b' => 2, 'a' => 1],
            ],
            '1D Array, 2D Array' => [
                [[1, 2], [3, 4]],
                [[2, 1], [4, 3]],
            ],
            '1D Array, 2D Hash' => [
                [['a' => 1, 'b' => 2], ['c' => 3, 'd' => 4]],
                [['b' => 2, 'a' => 1], ['d' => 4, 'c' => 3]],
            ],
            '1D Hash, 2D Array' => [
                ['a' => [1, 2], 'b' => [3, 4]],
                ['b' => [4, 3], 'a' => [2, 1]],
            ],
            '1D Hash, 2D Hash' => [
                ['a' => ['a1' => 11, 'a2' => 12], 'b' => ['b1' => 21, 'b2' => 22]],
                ['b' => ['b2' => 22, 'b1' => 21], 'a' => ['a2' => 12, 'a1' => 11]],
            ],
            'Array, Hash mixed' => [
                ['a' => ['a1' => 11, 'a2' => 12], 'b' => [21, 22]],
                ['b' => [22, 21], 'a' => ['a2' => 12, 'a1' => 11]],
            ],
        ];
    }

    /**
     * @test
     * @dataProvider data_assertArraySimilar_expected_failure
     * @param array $expected
     * @param array $actual
     */
    public function assertArraySimilar_expected_failure(array $expected, array $actual)
    {
        try {
            $this->assertArraySimilar($expected, $actual);
        } catch (\PHPUnit\Framework\ExpectationFailedException $e) {
            return;
        }
        $this->fail('Expected exception was not thrown.');
    }

    public function data_assertArraySimilar_expected_failure()
    {
        return [
            '1D Array' => [
                [1, 2, 3],
                [1, 2, 4],
            ],
            '1D Hash' => [
                ['a' => 1, 'b' => 2, 'c' => 3],
                ['a' => 1, 'b' => 2, 'd' => 3],
            ],
            '1D Array, 2D Array' => [
                [[1, 2], [3, 4]],
                [[1, 2], [3, 5]],
            ],
            '1D Array, 2D Hash' => [
                [['a' => 1, 'b' => 2], ['c' => 3, 'd' => 4]],
                [['a' => 1, 'b' => 2], ['c' => 3, 'e' => 4]],
            ],
            '1D Hash, 2D Array' => [
                ['a' => [1, 2], 'b' => [3, 4]],
                ['a' => [1, 2], 'b' => [3, 5]],
            ],
            '1D Hash, 2D Hash' => [
                ['a' => ['a1' => 11, 'a2' => 12], 'b' => ['b1' => 21, 'b2' => 22]],
                ['a' => ['a1' => 11, 'a2' => 12], 'b' => ['b1' => 21, 'b3' => 22]],
            ],
            'Array, Hash mixed' => [
                ['a' => ['a1' => 11, 'a2' => 12], 'b' => [21, 22]],
                ['a' => ['a1' => 11, 'a3' => 12], 'b' => [21, 22]],
            ],
        ];
    }
}
```


さいごに、冒頭で述べたように、本来、できることなら、順番も保証された上でのテストであったほうが良いとは思うので「なぜ期待した順番にならないのか。」というところも調査するのがいいのかもしれません。