忍者ブログ

Memeplexes

プログラミング、3DCGとその他いろいろについて

TypeScriptでJasmineのテストメソッドを増やす(カスタムマッチャー)

JavaScriptのプログラムをテストするのに役立つJasmineですが、TypeScriptで使おうとすると、ユーザー定義の型をテストするときに困ることがあります。「1と1.0000001がほぼ等しいかどうか」はテストできるのですが、「new THREE.Vector3(1, 0, 0)とnew THREE.Vector3(1.0000001, 0, 0)がほぼ等しいかどうか」をテストできるようにする方法はちょっとだけめんどくさいです。メモしておきます。


カスタム型のクラスをtoBeCloseToでテストする

Jasmineである数字が別の数字と十分に似ているかどうかをテストするにはtoBeCloseTo()を使います。

        toBeCloseTo(expected: number, precision?: any, expectationFailOutput?: any): boolean;

1と1.0000000001ならほとんど一緒なのでテストにパスします。似た名前のtoBe()メソッドではパスしません。ある程度の誤差を許したい時にメソッドを使います。

ところが困ったことがあって、このメソッドはnumber同士の比較はできるものの、ユーザー定義のベクトルなどは比較できません。3Dグラフィックの計算をするときはちょっとした誤差がつもりやすそうなので3次元のベクトルにこのメソッドを使いたいものです。しかしJasmineには基礎的なメソッドしか用意されていないので、自分でVector3用のtoBeCloseTo()を作るしかありません。

THREE.Vector3用のtoBeCloseTo()

Vector3Matcher.ts

/// <reference path="../Scripts/typings/threejs/three.d.ts" />
/// <reference path="../Scripts/typings/jasmine/jasmine.d.ts"/>

declare module jasmine {
    function addMatchers(customMatchers: any);

    interface Matchers {
        toBeCloseTo(expected: THREE.Vector3|number, precision?: number): void;
    }

    interface Matcher { }
    interface ToBeCloseToMatcher extends Matcher {
        compare(actual: any, expected: any, precision?: number): jasmine.CustomMatcherResult;
    }

    var matchers: Array<()=>Matcher>;
}

class Vector3CustomMatcher {
    originalToBeCloseToMatcher: jasmine.ToBeCloseToMatcher;

    constructor(originalToBeCloseToMatcher: jasmine.ToBeCloseToMatcher) {
        this.originalToBeCloseToMatcher = originalToBeCloseToMatcher;
    }
    
    compare(actual: any, expected: any, precision?: number): jasmine.CustomMatcherResult {

        if (!this.isVector3Test(actual, expected)) {
            return this.originalToBeCloseToMatcher.compare(actual, expected, precision);
        }
        
        return {
            pass: this.areCloseEnough(
                actual,
                expected,
                this.computeAcceptableError(precision)
            )
        };
    }

    private isVector3Test(actual: any, expected: any) {
        return (actual instanceof THREE.Vector3) && (expected instanceof THREE.Vector3);
    }

    private areCloseEnough(
        actual: THREE.Vector3,
        expected: THREE.Vector3,
        acceptableError: number) {

        return Math.abs(expected.x - actual.x) < acceptableError
            && Math.abs(expected.y - actual.y) < acceptableError
            && Math.abs(expected.z - actual.z) < acceptableError;
    }

    private computeAcceptableError(precision: number): number {

        if (precision !== 0) {
            precision = precision || 2;
        }

        return Math.pow(10, -precision) / 2;
    }
}


function addVector3Matcher() {
    var originalMatcher = jasmine.matchers["toBeCloseTo"]();
    var customMatcher = new Vector3CustomMatcher(originalMatcher);

    function compare(actual: any, expected: any, precision?: number):
        jasmine.CustomMatcherResult  {
        return customMatcher.compare(actual, expected, precision);
    };

    function createNewToBeCloseToMatcher (
        util: jasmine.MatchersUtil,
        customEqualityTesters: Array<jasmine.CustomEqualityTester>
    ) {
        return {
            compare: compare
        };
    };

    var customMatchers = {
        toBeCloseTo: createNewToBeCloseToMatcher
    };

    jasmine.addMatchers(customMatchers);
}

Vector3Test.ts

/// <reference path="../Scripts/typings/threejs/three.d.ts" />
/// <reference path="../Scripts/typings/jasmine/jasmine.d.ts" />
/// <reference path="Vector3Matcher.ts" />

describe("Vector3クラス",
    () => {

        beforeEach(() => {
            addVector3Matcher();
        });

        
        it("の (1, 0, 0) は (1.0000001, 0, 0) と等しい",
            () => {
                expect(new THREE.Vector3(1, 0, 0)).toBeCloseTo(new THREE.Vector3(1.0000001, 0, 0), 2);
            });
        it("の (1, 0, 0) は (1.1, 0, 0) と等しくない",
            () => {
                expect(new THREE.Vector3(1, 0, 0)).not.toBeCloseTo(new THREE.Vector3(1.1, 0, 0), 2);
            });

        it("1 は 1.0000001 と等しい",
            () => {
                expect(1).toBeCloseTo(1.0000001, 2);
            });
    });

これはVector3をテストしているコードです。new THREE.Vector3(1, 0, 0)がnew THREE.Vector3(1.0000001, 0, 0)と等しいか確認するテストです。

TypeScriptだとprecisionなどの引数を付け加えるときにめんどくさいですが、まあ型があるので一長一短ですね。

拍手[0回]

PR