Help us understand the problem. What is going on with this article?

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

概要

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

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

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

本題

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

<?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でも公開しています

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

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

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

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

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

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

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

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

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

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

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

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]],
            ],
        ];
    }
}

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

akkiihs
https://twitter.com/akkiihs
https://github.com/akkiihs
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away