ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

QUnit の deepEqual は異なるグローバル環境で生成されたオブジェクトの比較で失敗する

JavaScript で書かれたプログラムのテストの話。 QUnitdeepEqual を使って 2 つのオブジェクトの構造が同じであることを確認しようとテストを書いていたのだけれど、どうみても同じ構造の 2 つのオブジェクトなのに deepEqual のテストが通らない、ということがあってはまってしまった。

状況

テスト対象のオブジェクトの構造は、以下のように配列の中にオブジェクトが入っているという単純なもの。

var expected = [ { "AAA": 123, "BBB": 234 }, { "AAA": 343, "BBB": 324 } ];

テスト対象のオブジェクトは、別の JavaScript グローバル環境 (global environment) の中 (例えば別の window に付随する browsing context の中) で生成されたもので、一方の期待される値としてのオブジェクトはテスト用の JavaScript ファイルの中で生成されたものであった。 使用していた QUnitコミット e34ffb61 のもの

原因

原因は、言ってしまえば deepEqual が異なるグローバル環境で生成されたオブジェクト同士の比較に対応していないことであった。 deepEqual がオブジェクトの比較をしている部分のコードを見ると、オブジェクトの constructor プロパティの値が同じであるかどうかを比較している箇所があった。

    if ( a.constructor !== b.constructor ) {
        // Allow objects with no prototype to be equivalent to
        // objects with Object as their constructor.
            if ( !(( getProto(a) === null && getProto(b) === Object.prototype ) ||
                    ( getProto(b) === null && getProto(a) === Object.prototype ) ) ) {
                return false;
        }
    }

constructor プロパティの値が異なるオブジェクトである場合、(さらにプロトタイプに関して条件があるが) 基本的に 2 つのオブジェクトは deepEqual 的には同じでないとみなされてしまう。 今回の場合、別のグローバル環境で生成されたオブジェクトの constructor プロパティの値はそのグローバル環境における オブジェクト Object であるため、deepEqual 的に同じではないとみなされてしまったわけである。

ドキュメントを読んでなんとなくで仕様を認識するとこういうところではまってしまって良くないなー。 (しかし enumerable でないプロパティを無視するくせに constructor プロパティの値が同じかどうか比較するのはどういう意図なんだろうか。)

とりあえず JSON 文字列として書けるオブジェクトの比較をしたい場合は、一度 JSON.stringifyJSON.parse を通してから deepEqual に渡すのが (将来的な仕様変更にも強そうだし) 良さそうな気がする。

サンプルコード

テスト用のファイル本体が test.html ファイルで、別のグローバル環境を用意するためのファイルが test-frame.html。 qunit.js と qunit.cssgithub にあるコミット e34ffb61 のもの。

  • test.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>異なる browsing context で生成されたオブジェクトを deepEqual で比較すると失敗する</title>
  <link rel="stylesheet" href="qunit.css">
</head>
<body>
  <div id="qunit"></div>
  <script src="qunit.js"></script>
  <script>
    QUnit.asyncTest( "", function () {
        var frameElem = document.createElement( "iframe" );
        frameElem.onload = function () {
            var target = window[0].testobj;
            var expected = { "aaa": 12 };

            deepEqual( target, expected, "このテストは通らない" );

            deepEqual( JSON.parse(JSON.stringify(target)), expected,
                "このテストは通る" );

            target.constructor = expected.constructor = Object;
            deepEqual( target, expected,
                "両方のオブジェクトの constructor プロパティを無理やり一致させると通る" );

            QUnit.start();
        };
        frameElem.src = "test-frame.html" 
        document.body.appendChild( frameElem );
    } );
  </script>
</body>
</html>
  • test-frame.html ファイル
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <script>
    this.testobj = { "aaa": 12 };
  </script>
</body>
</html>