cucumberでデータベースに意味のあるダミーデータを入れよう!

皆さんは、ローカル開発環境や検証環境のダミーデータはどのように管理しているでしょうか？SQLファイルでしょうか？CSVでしょうか？お手製のスクリプトで用意でしょうか？データベースはアプリケーションの核です。単なるデータの列ではなく、どのような意味のデータであるかを管理しなければ十分な状況には対応出来ません。特にQAの方と協業するときには、どのようなデータを用意できるかでテストのカバーできる範囲が大きく異なるでしょう。今回はCucumberを利用したやり方を紹介してみます。

# Cucumberとは

[Cucumber](https://cucumber.io/docs)はビヘイビア駆動開発(BDD)や受け入れテスト駆動開発(ATDD)のために作られたツールです。[Gherkin Syntax](https://cucumber.io/docs/gherkin/reference/)と呼ばれる文法で自然言語で仕様を書き、それを自動テストツール(Selenium, JUnit)等とマッピングすることでステークホルダーとのやり取りに使うのが主の使われ方です。

```gherkin
Feature: Guess the word

  # The first example has two steps
  Scenario: Maker starts a game
    When the Maker starts a game
    Then the Maker waits for a Breaker to join
```

ここで発想を転換すると、「自然言語で何らかの処理を動かす」為のツールと解釈することができます。つまり何かを説明したり、宣言的な処理を書くのに優れたツールであると見ることが出来ます。データベースのダミーデータは、**自然言語で意味のある形で宣言的に誰でも管理できる**形にすべきです。

# ダミーデータのためのCucumberの利用

それではCucumberを利用したダミーデータの管理の例を紹介してきます。

## サンプルDB

今回はシンプルにユーザがつぶやくことが出来るシステムのデータベースを作ってみます。

```sql
drop database dummydb;
create database dummydb default character set utf8mb4;

use dummydb;

create table `users` (
  `user_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

create table `tweets` (
  `tweet_id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) unsigned NOT NULL,
  `content` varchar(300) NOT NULL,
  PRIMARY KEY (`tweet_id`),
  CONSTRAINT `FK_TWEETS_USER_ID_USER_USER_ID` FOREIGN KEY (`user_id`) REFERENCES `users` (`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

## feature

ダミーデータを作り出すシナリオは例えば以下のように用意することができます。1件だけのデータを作ることも、データテーブルを利用して複数件のデータを用意することも、書き方次第でどんなシナリオも用意することができます。もちろん日本語を利用することもできるので、エンジニア以外が他のシナリオを参考にしながらダミーデータを用意することができます。(実際にはデータの整合性を考えながら、非エンジニアの要望を聞いて、エンジニアが用意することになるとは思いますが、かなり効率化することが出来ると思います。)

```gherkin
# encoding: utf-8

Feature: テストデータ

  Scenario: つぶやきがないユーザを作る
    Given DBの設定をする
    When id 1 name "John" のユーザを作る
    Then コネクションを閉じる

  Scenario: つぶやきを持つユーザを作る
    Given DBの設定をする
    When id 2 name "Mike" のユーザを作る
    And つぶやきを作る
      | user_id | content |
      | 2       | hello   |
      | 2       | world   |
      | 2       | hoge    |
    Then コネクションを閉じる

  Scenario: パフォーマンステスト用に、つぶやきが多いユーザを作る
    Given DBの設定をする
    When id 3 name "God" のユーザを作る
    And user_id 3 のユーザに ランダムな内容1000個のつぶやきを作る
    Then コネクションを閉じる
```

## StepDefs

もちろんシナリオを書いただけでは魔法の様にダミーデータを作ってくれるわけではありません。裏で動く処理を用意してあげる必要があります。Java、JavaScript、Ruby、KotlinでCucumberは動かすことが出来ます。単にInsert文を生成して流すだけなので、今回はGradleとJavaという構成で動かしたいと思います。

ポイントとしては、```@Given("^DBの設定をする$")```や```@When("^id (\\d+) name \"([^\"]*)\" のユーザを作る$")```のように、featureとマッピングするためのアノテーションが付いていることです。正規表現を書く必要があるのか？と不安に思ったあなた。安心してください。これらのメソッドはCucumber側が自動生成してくれて、中身の処理だけを用意すれば大丈夫です。

```java
package gradle.cucumber;

import cucumber.api.DataTable;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import org.apache.commons.text.RandomStringGenerator;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.Optional;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class InsertDataStepDefs {
    private Connection connection;

    private String createUserBy(int id, String name) {
        return String.format("INSERT INTO `users` (user_id, name) VALUES (%d, '%s');", id, name);
    }

    private String createTweetBy(int user_id, String content) {
        return String.format("INSERT INTO `tweets` (user_id, content) " +
                "VALUES " +
                "(%d, '%s');", user_id, content);
    }

    private static String generateRandomString(int length) {
        return new RandomStringGenerator.Builder()
                .withinRange('0', 'z')
                .filteredBy(Character::isLetterOrDigit)
                .build()
                .generate(length);
    }

    @Given("^DBの設定をする$")
    public void setupDB() throws Throwable {
        String url = "jdbc:mysql://"
                + Optional.ofNullable(System.getenv("DB_HOST")).orElse("localhost")
                + ":"
                + Optional.ofNullable(System.getenv("DB_PORT")).orElse("3306")
                + "/dummydb";
        connection = DriverManager.getConnection(url,
                Optional.ofNullable(System.getenv("DB_USER")).orElse("root"),
                Optional.ofNullable(System.getenv("DB_PASSWORD")).orElse("root")
        );
    }

    @When("^id (\\d+) name \"([^\"]*)\" のユーザを作る$")
    public void createUser(int id, String name) throws Throwable {
        try {
            Statement statement = connection.createStatement();
            statement.execute(this.createUserBy(id, name));
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    @When("^つぶやきを作る$")
    public void createTweet(DataTable table) throws Throwable {
        Stream<Map<String, String>> tweets = table.asMaps().stream();
        tweets.map(tweet -> this.createTweetBy(
                Integer.valueOf(tweet.get("user_id")), tweet.get("content"))
        ).forEach(s -> {
            try {
                Statement statement = connection.createStatement();
                statement.execute(s);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        });

    }

    @When("^user_id (\\d+) のユーザに ランダムな内容(\\d+)個のつぶやきを作る$")
    public void createRandomTweets(int userId, int numOfTweet) throws Throwable {
        IntStream.range(0, numOfTweet).boxed()
                .map(i -> this.createTweetBy(userId, generateRandomString(200)))
                .forEach(s -> {
                    try {
                        Statement statement = connection.createStatement();
                        statement.execute(s);
                    } catch (SQLException e) {
                        e.printStackTrace();
                    }
                });
    }

    @Then("^コネクションを閉じる$")
    public void connectionClose() throws Throwable {
        connection.close();
    }
}
```

この記事の内容は以下のリポジトリで試すことが出来ます。ご自由にお使いください。

https://github.com/ababup1192/cucumber_insert_data

# まとめ

Cucumberは動く仕様を作るためのツールです。今回はダミーデータを管理するために使いましたが、使い方次第で色んなことに応用ができる非常に強力なツールです。特に自然言語とマッピングする機能やデータテーブルの機能(Gherkins)が強力なため自前のスクリプトを用意するよりも手軽に表現力が得られるため使わない手は無いと思います。色んな使い方を試してみましょう！