golangの日記

Go言語を中心にプログラミングについてのブログ

JavaScript class のプライベート関数/変数のアクセス方法と Node Test runner

js.png


JavaScript(es6)class のインスタンスからプライベート関数・変数を取得したい。テストしたいので。





プライベート関数・変数を取得するには classconstructor に以下のようにテストのときだけ内部にアクセスできる関数を動的に定義する。

# から始まるプライベートな変数・関数は this["#variable"] のように取得できないし、Object.getOwnPropertyDescriptors とか Object.getOwnPropertyNames にも無い。__proto__ とか prototype でも取得できない。他に方法があるのか検索しても分からなかったので仕方なく eval を使う。eval を使うこの関数の問題点としては internal("foo-bar") とか書くと NaN が返ってきたりする。正しく使わないと eval の解釈によって意図しないことが起こるかもしれない。

index.js

export default class A {
    publicDynamicVariable;
    #privateDynamicVariable;
    static #privateStaticVariable = "#privateStaticVariable-value";

    constructor() {
        this.#privateDynamicVariable = "#privateDynamicVariable-value";
        this.publicDynamicVariable = "publicDynamicVariable-value";

        // NODE_TEST が環境変数に定義されている場合に internal 関数を使えるようにする
        if (
            typeof process != "undefined" &&
            process["env"] &&
            process.env["NODE_TEST"]
        ) {
            this.internal = (name) => {
                let prop;
                // A はクラス名。A.staticMethod のように静的な変数・関数にアクセスする用
                const o = { this: this, A: A };
                for (const k in o) {
                    try {
                        prop = eval(`${k}.${name}`);
                    } catch (e) {
                        continue;
                    }
                    if (typeof prop != "undefined") {
                        if (typeof prop == "function") {
                            // 関数だったら this か A を bind する
                            return prop.bind(o[k]);
                        }
                        return prop;
                    }
                }
                // name がプロパティ名として存在しない場合はエラーをスローする
                throw new Error(
                    `not defined function or variable in class: internal("${name}")`
                );
            };
        }
    }

    publicDynamicFunction(a, b, c) {
        return [this.publicDynamicVariable, a, b, c];
    }

    #privateDynamicFunction(a, b, c) {
        return [this.#privateDynamicVariable, a, b, c];
    }

    static #privateStaticFunction(a, b, c) {
        return [A.#privateStaticVariable, a, b, c];
    }
}

// new A する前にこれ書いておけば internal 関数が使えるようになる。
process.env.NODE_TEST = true;

const a = new A();

let v;

// 以下 internal 関数を使って dynamic/static な public/private 変数関数を取得できる

v = a.internal("publicDynamicFunction")("foo", "bar", "baz");
console.log(v); // [ 'publicDynamicVariable-value', 'foo', 'bar', 'baz' ]

v = a.internal("#privateDynamicFunction")("foo", "bar", "baz");
console.log(v); // [ '#privateDynamicVariable-value', 'foo', 'bar', 'baz' ]

v = a.internal("#privateStaticFunction")("foo", "bar", "baz");
console.log(v); // [ '#privateStaticVariable-value', 'foo', 'bar', 'baz' ]

v = a.internal("publicDynamicVariable");
console.log(v); // publicDynamicVariable-value

v = a.internal("#privateDynamicVariable");
console.log(v); // #privateDynamicVariable-value

v = a.internal("#privateStaticVariable");
console.log(v); // #privateStaticVariable-value


Node Test runner を使ったテスト。npmパッケージを一切使ってないコードは、これを使ってテストを書けば依存が一切ないので良い。


ディレクトリ構成

$ tree
.
├── index.js
├── index.test.js
└── package.json


package.json の中身。export/import するために "type": "module" を書いておく

$ cat package.json
{
  "type": "module"
}


テストファイル。assertのドキュメントはここ

index.test.js

import assert from "node:assert/strict";
import { test, describe } from "node:test";
import A from "./index.js";

// internal 関数を使えるようにする
process.env.NODE_TEST = true;

describe("internal 関数のテスト", () => {
    const a = new A();

    test("不明なプロパティ名はエラーがスローされる", () => {
        assert.throws(() => a.internal("#unknown"), {
            message: `not defined function or variable in class: internal("#unknown")`,
        });
    });

    // それぞれの変数・関数が正しく取得できてるかどうかのテスト
    [
        {
            name: "publicDynamicFunction",
            args: [10, 20, 30],
            expected: ["publicDynamicVariable-value", 10, 20, 30],
        },
        {
            name: "#privateDynamicFunction",
            args: [10, 20, 30],
            expected: ["#privateDynamicVariable-value", 10, 20, 30],
        },
        {
            name: "#privateStaticFunction",
            args: [10, 20, 30],
            expected: ["#privateStaticVariable-value", 10, 20, 30],
        },
        {
            name: "publicDynamicVariable",
            expected: "publicDynamicVariable-value",
        },
        {
            name: "#privateDynamicVariable",
            expected: "#privateDynamicVariable-value",
        },
        {
            name: "#privateStaticVariable",
            expected: "#privateStaticVariable-value",
        },
    ].forEach((data, index) => {
        test(`${index}: Test ${data.name}`, () => {
            const v = a.internal(data.name);
            const actual = typeof v == "function" ? v(...data.args) : v;
            assert.deepStrictEqual(actual, data.expected);
        });
    });
});


テストの実行と結果

$ node --test

▶ internal 関数のテスト
  ✔ 不明なプロパティ名はエラーがスローされる (1.4184ms)
  ✔ 0: Test publicDynamicFunction (0.6888ms)
  ✔ 1: Test #privateDynamicFunction (0.1795ms)
  ✔ 2: Test #privateStaticFunction (1.2162ms)
  ✔ 3: Test publicDynamicVariable (0.2144ms)
  ✔ 4: Test #privateDynamicVariable (0.1314ms)
  ✔ 5: Test #privateStaticVariable (0.1674ms)
▶ internal 関数のテスト (7.1375ms)

ℹ tests 7
ℹ suites 1
ℹ pass 7
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 85.036409