LoginSignup
28
18

More than 5 years have passed since last update.

爆発的にパターンが増えるテストデータはBuilderで作ろう

Last updated at Posted at 2016-11-21

データベースのモック、作ったAPIのIN/OUTのダミーデータ、外部システムのモック、用途はなんでも良いけどテストデータってしょっちゅう作るじゃん。
でも保守は面倒だし正しく使うのも使わせるのも大変だし、どうにかならんもんかとちょっと考えてみた。

型型したいのでサンプルコードはJava、でも別に何の言語でも良いと思う。

お題

仮に契約というデータがあり、内容が以下の様な感じだとする。

{userName=ほげ太郎, plan=NORMAL, limit=UNLIMITED, items=[Item1, Item2], coupon=}
key value
userName 必須
plan NORMAL/PREMIUM
limit UNLIMITED/LIMITED
items 0..*
coupon NORMAL時はなし、PREMIUM時は必須

このハッシュを作る方法を考えようと言う事。

方法

過去に所属したチームでは.xml.json.txtで全パターン作って用意しておいたり、以下の様なJavaで書かれたテストデータもあった。

public static Map<String, Object> 商品複数_割引なし() {
    Map<String, Object> map = new HashMap<>();
    map.put("userName", "ほげ太郎");
    map.put("plan", "NORMAL");
    map.put("limit", "UNLIMITED");
    map.put("items", Arrays.asList("Item1", "Item2"));

    return map;
}

それらに共通する問題は「全パターン用意すると膨大になる点」と「couponはNORMAL時はなし、PREMIUM時は必須の様な仕様が漏れ易い点」だ。

テキトーに数えてみても、例えばitems0件1件2件の場合を用意するとして
plan2種 x limit2種 x items3種 = 12件の用意が必要になってしまう。

数年の仕様変更を経てそれにplan2種支払い方法3種が増えたりして、LIMITEDの場合はコンビニ決済は不可能、なんてなってきたら絶対に破綻する、断言する。

Builderを作る

Builderを使って部分部分を組み立てる方法だけ用意し、使う側が欲しい様に組み立てられる様にしてみる。
また、最終的にMapが欲しいからと言って内部での中間生成状態までMapである必要もない。

public class ContractBuilder {
    private String userName;
    private Plan plan;
    private Limit limit;
    private List<String> items = new ArrayList<>();
    private Optional<String> coupon = Optional.empty();

    public ContractBuilder userName(String userName) {
        this.userName = userName;

        return this;
    }

    public ContractBuilder normalPlan() {
        this.plan = Plan.NORMAL;

        return this;
    }

    public ContractBuilder premiumPlan(String coupon) {
        this.plan = Plan.PREMIUM;
        this.coupon = Optional.of(coupon);

        return this;
    }

    public ContractBuilder limited() {
        this.limit = Limit.LIMITED;

        return this;
    }

    public ContractBuilder unlimited() {
        this.limit = Limit.UNLIMITED;

        return this;
    }

    public ContractBuilder item(String item) {
        this.items.add(item);

        return this;
    }
}

この方針だと全てのパターンをこれだけの行数で網羅することが出来る。
一気にフル要素を手に入れるのではなく、各要素をだんだんと組み立てて行くイメージで生成する。
で、最後に一度だけ欲しい形に変換して吐き出せば良い。

    public Contract build() {
        return new Contract(userName, plan, limit, items, coupon);
    }

    public Map<String, Object> toMap() {
        Map<String, Object> map = new HashMap<>();

        map.put("userName", userName);
        map.put("plan", plan);
        map.put("limit", limit);
        map.put("items", items);
        map.put("coupon", coupon.orElse(""));

        return map;
    }

保持している値を好きな形に一度だけ変換すれば良いので、Map以外にもすぐ吐ける。

thisを返しているので、こんな感じで連結しながら使う

Map contract = new ContractBuilder()
        .userName("ほげ太郎")
        .normalPlan()
        .limited()
        .item("Item1")
        .item("Item2")
        .toMap();
Map contract = new ContractBuilder()
        .userName("ほげ太郎")
        .premiumPlan("ABC")
        .unlimited()
        .item("Item1")
        .item("Item2")
        .toMap();

 メリット

先述の通り、これだけで全パターンが網羅できるし例えば支払い方法が2パターン増えても、メソッドを2つ増やすだけで対応が済む。

もう1つのポイントは、例えばpremiumPlancouponを引数に取るので「PREMIUMだけどcouponの設定を忘れる」という事がないし、
limited()の場合のみになんらかの条件を書き足す」という事が容易であること。

デメリット

利用する側が正しく組み立てる必要があるので、不正な状態を生成してしまう可能性がある。

Map contract = new ContractBuilder()
        .toMap();
Map contract = new ContractBuilder()
        .userName("ほげ太郎")
        .normalPlan()
        .normalPlan()
        .normalPlan()
        .toMap();

これはどこかでぬるぽを起こすので避けたい。

また、エディタの補完対象を見ても今一使い方がわからない点も問題だ。
例えば「normalPlan()premiumPlan()が排他なのか」、「planlimitは排他なのか両方設定するべきなのか」、「順番やメソッド間の依存はあるのか」、「何を満たせばtoMapを実行して大丈夫なのか」と言ったことが察せない。

builder.png

1要素を選択する毎に異なるクラスを返す

上記の問題を解決するには、return this;の所をそれぞれ違う型にする方法がある。

public class ContractStrictBuilder {
    private String userName;
    private Plan plan;
    private Limit limit;
    private List<String> items = new ArrayList<>();
    private Optional<String> coupon = Optional.empty();

    public _Plan userName(String _userName) {
        userName = _userName;
        return new _Plan();
    }

    public class _Plan {
        public _Limit normalPlan() {
            plan = Plan.NORMAL;

            return new _Limit();
        }

        public _Limit premiumPlan(String _coupon) {
            plan = Plan.PREMIUM;
            coupon = Optional.of(_coupon);

            return new _Limit();
        }
    }

    public class _Limit {
        public _Item limited() {
            limit = Limit.LIMITED;

            return new _Item();
        }

        public _Item unlimited() {
            limit = Limit.UNLIMITED;

            return new _Item();
        }
    }

    public class _Item {
        public _Item item(String item) {
            items.add(item);

            return new _Item();
        }

        public Map<String, Object> toMap() {
            Map<String, Object> map = new HashMap<>();

            map.put("userName", userName);
            map.put("plan", plan);
            map.put("limit", limit);
            map.put("items", items);
            map.put("coupon", coupon.orElse(""));

            return map;
        }
    }
}

この様に2択を迫る異なるクラスを次々と返す事で利用者は正しく組み立てることが出来る。

メリット

下のキャプチャが示す通り、「必ずこの2つから選ぶ」「ここまで来たらtoMapを実行して良い」という事がエディタに教えてもらえるのが最大のメリット。

まずuserNameを必ず設定する
strict_userName.png

次はplanを必ず設定する
この際にPREMIUMの場合は必ずcouponを設定する
strict_plan.png

次はlimitを必ず選択する
strict_limit.png

次はitemsの設定だが、toMapも実行出来るのでitemsは任意項目である事がわかる
strict_items.png

気が済んだらtoMapを呼ぶ
strict_toMap.png

デメリット

当然だが実装コストはかかる。

まとめ

2つのBuilderを考えてみたが、どちらが優れているかという話ではなくて適材適所だと思う。

ただ後者の例はエディタで次々と「どっち?」「どっち?」って聞かれるのでなんか楽しくて好きw
使うだけなら圧倒的に後者が使い易いしね。

そんな訳でぜひ良いパターン管理を
おつノシ

28
18
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
18