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

読みやすいテストを考えてみる

More than 3 years have passed since last update.

テストを作る事は良くあると思いますが、
書いている時はよいのですが、後で見返して、これは何のテストだろーってなる事はあるあるだと思います。

今回は、そんな「何をしたかったのか分からな~い」をどれだけ緩和できるかにチャレンジしたいと思います。


いきなり結果から見せちゃいます。

左ペインにはTestNGのレポートが出ています(エラーはわざとですよ!!)。
右ペインは実際に動かしたテストソースです。

実行結果.png

解説

まず、左ペインを見てもらえれば分かると思いますが、

今回はテストケース1つ1つを人の読める形で出力させています。(今回は日本語ですが、英語でもフランス語でも平気です)

こうする事で、どういったテストが成功して、どういったテストが失敗したかがすぐに分かるようになります。
レポートとして日々確認する必要がある場合は、何が失敗したかがすぐ分かる事は結構重要だったりします。
(作成者の名前も併せるとチーム開発の現場などでは報告しやすくなります。名前の入れ方はこの後の作り方を見て頂ければ拡張方法も分かると思います。)

とはいえ、これを作るのが大変だと本末転倒ということで、右ペインに移ります。

見て頂きたいのは、1つ1つのテストは、

situation("テストのタイトル", () -> {

   テストの内容

})

という少ないフォーマットで書けるようになっているとうことです。

また、タイトルをテスト個々の頭に持ってくることで、何をしているのかを瞬時に判断しやすいようにしました。

実装方法

では、実際にこれをどう実現しているかを見てみます。

必要なのは、TestNG と、下記 「Situation.java」、「TestFrame.java」となります。

あとは、「TestFrame.java」を実装したテストケースを個々に考えて作ってください。

Situation.java
package test.test.test.test;

/**
 * Case という名前だと予約語と被って色々面倒なので Situation.
 * テストケースとして、title と 実行する test を記述した Runnable を保持しています.
 */
public class Situation {

    private final String title;
    private final Runnable test;

    public Situation (String title, Runnable test) {
        this.title = title;
        this.test = test;
    }

    /**
     * Runanalbe を実行しているだけ.
     */
    public void test() {
        test.run();
    }

    /**
     * TestNG の結果には、DataSouce で渡される引数を toString() した物が出るようなので、タイトルを出力するようにする.
     */
    @Override
    public String toString() {
        return title;
    }

}

基本的には、JavaDocコメントにある通りです。
大事なのは、toString() でタイトルを返すこで、TestNGで実行した際のレポートにタイトルを表示させるようにしている事です。

TestFrame.java
package test.test.test.test;

import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

/**
 * abstract class に @Test と @DataProvider を実装しておき、
 * {@link Situation} の配列を取得する部分のみサブクラスで実装するようにしています.
 */
public abstract class TestFrame.java {

    /**
     * {@link Situation} の実行のみ行う.
     */
    @Test(dataProvider = "datasouce")
    public void test(Situation situation) {
        situation.test();
    }

    /**
     * 以下の様な {@link Situation} の二次元配列を生成し、{@link DataProvider} として返してます.
     * <br>
     *
     * new Situation[] {
     *     { new Situation(...) },
     *     { new Situation(...) },
     *     ...
     *
     * }
     */
    @DataProvider(name = "datasouce")
    public Object[][] source() {
        Situation[] cases = situations();
        Object[][] sources = new Object[cases.length][1];
        for (int i = 0; i < cases.length; i++) {
            sources[i][0] = cases[i];
        }
        return sources;
    }

    public Situation situation(String name, Runnable test) {
        return new Situation(name, test);
    }

    /**
     * テストケースとして、{@link Situation} の配列を返してください.
     */
    protected abstract Situation[] situations();

}

こちらも基本的な事は、JavaDoc コメントに書いてあります。
ここでは、@Test、@DataProvider をここに書くことで、このクラスを継承するサブクラスが自動的にテストを規定動きに沿って処理させられるようにしています。


あとは、TestFrame.java を継承したサブクラスをつくって、そこに Situation をいっぱい追加してあげれば完成です。

TestImpl.java
package test.test.test.test;

/**
 * {@link TestFrame} の実装.
 * テストケースとして、{@link Situation} を生成して返します.
 */
public class TestImpl extends TestFrame {

    /**
     * ここにテストを実装.
     */
    @Override
    protected Situation[] situations() {

        return new Situation[] {

                situation("1 + 1 ", () -> {
                    int i = 1;
                    i = i + i;
                    System.out.println(i);
                }),


                situation("Fail するよ", () -> {

                    org.testng.Assert.fail("正しくエラーになりました.");

                }),


                situation("ラムダ式で書かない場合.", new Runnable() {

                    @Override
                    public void run() {
                        int i = 1;
                        i = i + i;
                        System.out.println(i);
                    }
                }),



                situation("改行することも"
                        + "一応できます.", () -> {

                    int i = 1;
                    i = i + i;
                    System.out.println(i);

                }),

        };
    }

    /**
     * {@link TestFrame} を実装することで、インナークラスでも動作可能.
     */
    public static class Impl2 extends TestFrame {

        @Override
        protected Situation[] situations() {

            return new Situation[] {

                    situation("インナークラスにしてみる.", () -> {
                        int i = 1;
                        i = i + i;
                        System.out.println(i);
                    })
            };
        }
    }
}

TestNGという既存フレームワークにちょっと手心を加えてみました。
Jenkins の TestNG プラグイン を使って、CI環境に組み込むとより効果を発揮してくれるかと思います。

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
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  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