mike-neckのブログ

Java or Groovy or Swift or Golang

簡単な特徴もないJava用のテスティングフレームワークつくった

諸事情により、JUnitが使えない状況でコードを書いていたのですが、書いたコードが動くかどうか確認したい衝動に駆られて、簡単な特徴もないテスティングフレームワークを作りました。

仕様

最初に書いた仕様がこれ。

github.com

public class MaybeTest extends Test {
    @Examination
    public void mapStringLengthRemainsSome() {
        setup(() -> some("stringLength=15"))
                .when(maybe -> maybe.map(String::length))
                .then(equalsTo(15));
    }
}

そのときはこんなイメージでした。

  • setup(Supplier<? extends T>)でテスト対象のオブジェクトを生成する
  • when(Function<? super T, ? extends N>)でテスト対象のオブジェクトに操作を加えていく
  • thenでアサートの準備
  • equalsTo(V)で比較を実施

なお、作っていく最中でthenメソッドthen(Function<? super T, ? extends V>)に変更して、実際の値を取得するように変更して、その後のequalsTo(V)で比較をする形に変更しました。

テストスイート

あと、テストクラスをスキャンしていくのは面倒だったし、作りこみたくなかったので、

TestSuite.run(Class<? extends Test>...)

というメソッドでテストしたいクラスだけ指定してテストするようにしました。

テスト結果の表示

ある程度出来上がった所で、テスト結果くらいは見栄え良く表示したいなと思い、

mike-neck.hatenadiary.com

これでコンソールの文字列の色の変え方を勉強しつつ、表示用のクラスの仕様を適当に書いておきました。

github.com

で、ある程度、テスト表示用のクラスを書いた所で、最後に表示の仕方を間違えないように次のようなコメントを書いてから実装しました。

    private static class SuccessPrinter extends AbstractPrinter {
        @Override
        public void print() {
            // クラス名(緑)
            // 以下繰り返し
            //     - テスト名(緑)
        }
    }

    private static class FailurePrinter extends AbstractPrinter {
        @Override
        public void print() {
            // クラス名(赤)
            // 以下繰り返し
            // Successの場合
            //     - テスト名(緑)
            // Failureの場合
            //     - テスト名(赤)
            // diff(赤)
            // Accidentの場合
            //     - テスト名(黄)
            // explanation(normal)
            // Panicの場合
            //     - テスト名(黄)
            // cause(normal)
        }
    }

感謝

テストの実行自体をExecutorServiceでクラスごとに並行して実行させたくて、かつ終了したら次の処理(テスト結果表示→テスト結果集計→集計結果表示)をやろうとして、CompletableFuture.allOf(CompletableFuture<?>...)を使おうとしたわけですが、StreamRunnableをボコボコ生成して、CompletableFuture.runAsyncに渡してCompletableFuture<Void>を生成してて、これを配列化しようとしていた所、new CompletableFuture<Void>[size]をしようとすると、Generic Array creationにひっかかってしまって、コンパイルエラーになるという事態が発生。

対策としてclass VoidFuture extends CompletableFuture<Void>{}というクラスを作って、new VoidFuture[size]をやりましたが、こんどはArrayStoreExceptionが発生(VoidFutureCompletableFuture<Void>ではないので、配列に詰め込むことができない)。

テンパッて壁を殴って4個くらい穴を開けながらツイートしていた所、

@makingさんうらがみさんから、いろいろと助言をいただきました。

結果的に、CompletableFuture.allOf(CompletableFuture<?>...)に対して次のようにすることで、すべてのテスト結果の同期を図ることができました。

    public CompletableFuture<Void> run() {
        return CompletableFuture.allOf(tests.stream()
                .map(createCases)
                .<Runnable>map(runners(queue))
                .map(r -> CompletableFuture.runAsync(r, exec))
                .collect(toList()).toArray(new CompletableFuture<?>[tests.size()]));
    }

new CompletableFuture<?>[size]だとGeneric array creationに引っかからないんですね…

まあ、お二方ともご協力ありがとうございます。

できた代物

で、こんな感じのテストができました。

テストスイートにテストを指定する
public class Main {
    public static void main(String[] args) {
        TestSuite.run(NothingTest.class, SomeTest.class);
    }
}
テストクラスそのもの
import static com.sample.data.api.Maybe.some;
import static com.sample.data.api.Maybe.nothing;

public class SomeTest extends Test {
    @Execute
    public void someIsSomeReturnsTrue() {
        setup(() -> some(20))
                .then(Maybe::isSome)
                .equalsTo(true);
    }
    @Execute
    public void mapperReturnsNullOnMapItBecomesNothing() {
        setup(() -> some("foo"))
                .when(m -> m.map(s -> null))
                .then(Maybe::isSome)
                .equalsTo(false);
    }
    @Execute
    public void someFmapedToNothingThenItBecomesNothing() {
        setup(() -> some(-5))
                .when(m -> m.fmap(i -> i < 0 ? nothing() : some(i)))
                .then(Maybe::isSome)
                .equalsTo(false);
    }
    @Execute
    public void someNotFilteredRemainsSome() {
        setup(() -> some("someNotFiltered"))
                .when(m -> m.filter(s -> s.startsWith("some")))
                .then(Maybe::isSome)
                .equalsTo(true);
    }
    @Execute
    public void someOrReturnsOriginalValue() {
        setup(() -> some("gene kelly"))
                .when(m -> m.map(CAPITALIZE))
                .then(m -> m.or("Frank Sinatra"))
                .equalsTo("Gene Kelly");
    }

    private static final Function<String, String> CAPITALIZE = s -> {
        boolean toUpper = true;
        char space = ' ';
        StringBuilder sb = new StringBuilder();
        for (char c : s.toCharArray()) {
            sb.append(toUpper ? Character.toUpperCase(c) : c);
            toUpper = c == space;
        }
        return sb.toString();
    };
}

テスト実行

これ、JUnitを使えない事情の原因は、このGradleプロジェクトがJVM components modelを採用していて、現状のGradle2.9ではExternal dependencyを解決できないことだったわけです。

というわけで、この簡単な特徴もないテスティングフレームワークJVM component modelのコンポーネントの一つとして実装しています。

で、実行するためには、JVM components modelで作成される大量のjarファイルを集めてclasspathに指定しないといけないわけで、Gradleのカスタムタスクを作成して実行するように出来ています。

task dataTestExec(type: JavaExec, dependsOn: 'assemble') {
    classpath = files(tasks.assemble.dependsOn.findAll {it instanceof BaseBinarySpec}.collect {
        [it.jarFile, it.apiJarFile]
    }.flatten().collect {it.absolutePath})
    main = 'com.sample.Main'
}

で、これを実行すると次のようにテストが実行されます。

f:id:mike_neck:20151121231945p:plain

ちなみに、上の例では全部のテストが通っていますが、テスト失敗するとこんな表示になります。

f:id:mike_neck:20151121232715p:plain

と、まあ、簡単で(欝でやる気でないので2日かかりましたが…)至って何の特徴もないテスティングフレームワークが完成しました。

こいつの公開等は、JVM components modelのpublishingがまだ未実装っぽいので、できても先の話になるでしょうし、はっきり言ってJUnitTestNG、Spockの方が明らかに高機能なので、公開するつもりはありません。

リポジトリー

ここ

github.com


おわり