Spring TestでEmbedded MySQLを使う方法


ちょっと前に竹添さん（@takezoen）がブログで紹介していた、`Embedded MySQL`を`JUnit`で使う方法です。

 * [組み込みMySQLを使ったユニットテスト](http://takezoe.hatenablog.com/entry/2016/05/26/100052)




簡単にできたけど、今後同じように迷わないように備忘録として残します。

### 各バージョン
訳あって`Spring`のバージョンが`3.2.8.RELEASE`ですが、4系の最新版でも問題ないです。

 * Java
 1.8.0 update 92
 
 * Spring Framework
 3.2.8.RELEASE
  
 * [wix-embedded-mysql](https://github.com/wix/wix-embedded-mysql)
 1.0.3

 * JUnit
 4.12

### やり方1（`JUnit`の`@ClassRule`を使う方法）
``` java:EmbeddedMySQL.java
package jp.ijfumi.test.db;

import com.wix.mysql.EmbeddedMysql;
import com.wix.mysql.config.MysqldConfig;
import org.junit.rules.ExternalResource;

import static com.wix.mysql.EmbeddedMysql.anEmbeddedMysql;
import static com.wix.mysql.config.MysqldConfig.aMysqldConfig;
import static com.wix.mysql.distribution.Version.v5_6_21;

public class EmbeddedMySQL extends ExternalResource {
    private EmbeddedMysql mysqld;

    protected void before() {
        if (mysqld == null) {
            MysqldConfig config = aMysqldConfig(v5_6_21)
                    .withPort(3306)
                    .withUser("root", "password")
                    .build();

            mysqld = anEmbeddedMysql(config)
                    .addSchema("test")
                    .start();
        }
    }

    protected void after() {
        mysqld.stop();
        mysqld = null;
    }
}
```
を作成し、テストクラスにて`@ClassRule`を指定するだけです。

```java:SampleTest.java
package jp.ijufumi.sample.test;

import jp.ijfumi.test.db. EmbeddedMySQL;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.ExternalResource;

public class SampleTest {
  @ClassRule
  public static ExternalResource resource = new EmbeddedMySQL();
}
```

ただ、これだと`MySQL`を使わないテストケースだけ実行する場合でも、`MySQL`を起動してしまいます。

### やり方2（`TestExecutionListener`を使う方法）

``` java:EmbeddedMySQLConfig.java
package jp.ijufumi.sample.test.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Inherited
@Documented
/**
 * テスト実行時に組み込みMySQLを使用する時に設定情報を指定するアノテーション。
 */
public @interface EmbeddedMySQLConfig {
    /** ポート番号 */
    int port() default 3306;

    /** 接続ユーザ */
    String user() default "root";
    /** 接続ユーザパスワード */
    String password() default "password";
}
```

と、

```java:EmbeddedMySQLTestExecutionListener.java
package jp.ijufumi.sample.test.support;

import jp.ijufumi.sample.test.annotation.EmbeddedMySQLConfig;

import com.wix.mysql.EmbeddedMysql;
import com.wix.mysql.config.MysqldConfig;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.support.AbstractTestExecutionListener;

import static com.wix.mysql.EmbeddedMysql.anEmbeddedMysql;
import static com.wix.mysql.config.MysqldConfig.aMysqldConfig;
import static com.wix.mysql.distribution.Version.v5_6_21;

/**
 * EmbeddedMySQLを起動させるExecutionListenerクラス。
 * 使う場合は、対象のクラスやメソッドに@EmbeddedMySQLConfigアノテーションを指定する。
 */
public class EmbeddedMySQLTestExecutionListener extends AbstractTestExecutionListener {
    private EmbeddedMysql mysqld;
    private ExecuteType type;

    public void beforeTestClass(TestContext testContext) throws Exception {
        EmbeddedMySQLConfig config = testContext.getTestClass().getAnnotation(EmbeddedMySQLConfig.class);
        if (config != null) {
            start(config.port(), config.user(), config.password());
            type = ExecuteType.CLASS;
        }
    }

    public void beforeTestMethod(TestContext testContext) throws Exception {
        EmbeddedMySQLConfig config = testContext.getTestMethod().getAnnotation(EmbeddedMySQLConfig.class);
        if (config == null) {
            return;
        }
		if (mysqld == null)
        {
            start(config.port(), config.user(), config.password());
            type = ExecuteType.METHOD;
        }
    }

    public void afterTestMethod(TestContext testContext) throws Exception {
        if (type != null && ExecuteType.METHOD.equals(type)) {
            stop();
        }
    }

    public void afterTestClass(TestContext testContext) throws Exception {
        stop();
    }

    private void start(int port, String user, String password) {
        if (mysqld == null) {
            MysqldConfig config = aMysqldConfig(v5_6_21)
                    .withPort(port)
                    .withUser("root", "")
                    .withUser(user, password)
                    .build();

            mysqld = anEmbeddedMysql(config)
                    .addSchema("local_jp_cotoco")
                    .start();
        }
    }

    private void stop() {
        if (mysqld != null) {
            mysqld.stop();
            mysqld = null;
        }
    }

    enum ExecuteType {
        CLASS,
        METHOD;
    }
}
```

を作成し、テスト対象に指定するだけです。


```java:SampleTest.java
package jp.ijufumi.sample.test;

import jp.ijufumi.sample.test.annotation.EmbeddedMySQLConfig;
import jp.ijufumi.sample.test.support.EmbeddedMySQLTestExecutionListener;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;

import org.junit.runner.RunWith;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath*:META-INF/spring/applicationContext-test.xml"})
@TestExecutionListeners({
        EmbeddedMySQLTestExecutionListener.class
})
@EmbeddedMySQLConfig
public class SampleTest {
}
```

### おわりに
個人的には、やり方2の方が好みです。