mike-neckのブログ

Java or Groovy or Swift or Golang

JSUG勉強会 2017 - 3 〜ドメイン駆動設計 powered by Spring のメモ

JSUGの勉強会に参加したので、そのメモ。

jsug.doorkeeper.jp

資料は公開されるだろうから、ここでしか聞けないことを中心にメモ(ツイート)したつもりだが、資料に盛り込まれている可能性は十分にある。なお、資料は以下の通り。

www.slideshare.net


個人的な感想

  • ドメイン駆動のセッションや話は「ドメイン駆動はいいぞ」という感じで抽象的な話になりがちなことが多いのだが、今回はフレームワークが限定されたこともあってどのようなオブジェクトをどのように作っていくという具体的な話が聞けて満足だった

  • ビジネスルールとドメインロジック
    • ビジネスルール(契約・法令・明文化されていない商習慣)などがソフトウェア開発の分析対象(もちろん、ビジネスルールがゲームのルールだったりサービスのルールだったりもする)
    • その分析結果をプログラミング言語で表現したものがドメインロジック

  • ソフトウェア開発のプロセス
    • いわゆるウォーターフォールのプロセス(情報収集・分析・設計・実装・テスト・リリース・運用)に加えてモデリングをおこなう
    • モデリングすると無理・無駄・ムラが減る
    • しかしモデリングするから仕様を決めなくてよいというわけではない
    • これらのプロセスをすべて毎日やる
  • Spring Frameworkドメイン駆動
  • いわゆるウォーターフォール(個人の所感を含む)

    • 一般的な文脈で言うSIerウォーターフォールは「伝言ゲーム」
    • 「伝言ゲーム」 = 顧客へのヒアリングする人と設計する人と開発する人が違うような開発
      • まあ、規模が大きいからと言って数百人とか用意しているようなプロジェクトだと、数百人で顧客のところへ押しかけても顧客が迷惑するだけですけどね…
    • もちろんフレームワークを何を選定するかはプロジェクトの自由なので、Spring Bootで「伝言ゲーム」開発も可能
  • ソフトウェアの構成

トランザクションスクリプト = ドメインモデルを構築しない手続きだけを書いたソフトウェア

トランザクションスクリプト型のSpringアプリケーションの構成

f:id:mike_neck:20170329221918p:plain

ドメイン駆動設計によるSpringアプリケーションの構成

f:id:mike_neck:20170329222009p:plain

ドメイン駆動でアプリケーションを作ると、各層とは別にドメインモデルの層を用意して、ここをインクリメンタルに成長させていく

  • ドメインモデル
    • ビジネスロジックの要素は次のものしかない
      • 数値
      • 日付
      • 文字列
    • オブジェクトの種類は次の3パターン
      • 値オブジェクト(数値/日付/文字列をラップしただけのオブジェクト、いわゆるValue Object)
      • 区分オブジェクト(振る舞いをもった enum)
      • コレクションオブジェクト(ListSet をラップしたオブジェクト(ListSet には直接触れさせないで、必要な操作だけを提供する))
  • モジュールの単位
  • ドメインオブジェクトとその他の層
  • ソースコードの文書化

    • メソッド名、クラス名だけでなく、メソッドの返す型、引数の型にはJava APIが出てこないようにして、ソースコードが自然とドメインを語るようにする
  • 具体的なドメインモデルによるSpringアプリケーションの構成

    • HTML ⇔ Controller ⇔ ドメインオブジェクト(プレゼンテーション層)
      • コントローラーはドメインオブジェクトを引数に取るようにする
      • 表示処理(〜の場合には表示する/しない)の演算をプレゼンテーション層には書かない(すべてドメインモデルにておこなう)
      • ドメインオブジェクトにはgetter/setterを設けず、 DirectFieldAccess を有効化してフィールドアクセスする
    • サービス層(アプリケーション層)
      • サービス層(アプリケーション層)には @Validated を引数のドメインオブジェクトにつけておいて、契約による設計を実現させる
    • データソース層
      • データベース層とドメインオブジェクトは別物ゆえ、マッピングを作っている(MyBatis使用)
      • ドメインオブジェクトはロジックの置き場なので、テーブルの論理を持ち込まない
      • データベース層も更新が楽になる、かつデータベースの論理を徹底できるように、きっちり正規化する

後半のトークセッション

ドメインモデルの用語はどこから取り入れているのかという質問に対して

同じく名前についてわたびきさんのエピソード

ロジックの順序について

既存コードが有る状態でのドメイン駆動の取り込み方 : 少しずつ入れていく

Javaでの和暦の扱いについて

Date and Time APIでの和暦の扱いをちゃんと調べていなかったので、調べた。

なお、以下で用いるコードにおいては次のフォーマッタおよび、タイムゾーンを使った。

final ZoneId tokyo = ZoneId.of("Asia/Tokyo");

final DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendText(ChronoField.ERA)
        .appendText(ChronoField.YEAR_OF_ERA)
        .appendLiteral("年")
        .appendText(ChronoField.MONTH_OF_YEAR)
        .appendText(ChronoField.DAY_OF_MONTH)
        .appendLiteral("日")
        .toFormatter(Locale.JAPAN);

final DateTimeFormatter iso = DateTimeFormatter.ISO_DATE;

1.和暦を扱う場合は以下のクラスを使う。

  • java.time.chrono.JapaneseDate
  • java.time.chrono.JapaneseEra

2.日付インスタンスの作り方

  • 日付を指定する場合は JapaneseDate#of(JapaneseEra, int, int, int) あるいは JapaneseDate#of(int, int, int) を使う。
  • 現在時刻が欲しい場合は JapaneseDate#now または JapaneseDate#now(ZoneId) または JapaneseDate#now(Clock) を使う。
  • 他の日付型から変換したい場合は JapaneseDate#from(TemporalAccessor) を使う。

というわけで、試してみましょう。まず、東京の今の日付が欲しい場合は次のようになります。

final JapaneseDate now = JapaneseDate.now(tokyo);
System.out.println(now.format(formatter));

結果

平成29年3月23日

昭和最後の日付近辺

final JapaneseDate showa = JapaneseDate.of(1989, 1, 7);
System.out.println(showa.format(formatter));
final JapaneseDate heisei = JapaneseDate.of(1989, 1, 8);
System.out.println(heisei.format(formatter));

結果

昭和64年1月7日
平成1年1月8日

明治は6年以前というのがないそうです

try {
  final JapaneseDate meiji = JapaneseDate.of(1872, 12, 31);
  System.out.println(meiji.format(formatter));
} catch (Exception e) {
  System.out.println(e.getClass().getCanonicalName());
  System.out.println(e.getMessage());
  System.out.println("1872/12/31");
}

結果

java.time.DateTimeException
JapaneseDate before Meiji 6 is not supported
1872/12/31

というわけで、明治6年以前は出力されないので、 厳密 にJIS X 0301に対応している模様です。


同様に和暦を変換していきます。

final JapaneseDate h = JapaneseDate.of(平成, 29, 1, 1);
System.out.println(h.format(iso));
final JapaneseDate m = JapaneseDate.of(明治, 6, 1, 1);
System.out.println(m.format(iso));
try {
  JapaneseDate.of(明治, 5, 12, 31);
} catch (Exception e) {
  System.out.println(e.getClass().getCanonicalName());
  System.out.println(e.getMessage());
  System.out.println("明治5年12月31日");
}

結果

2017-01-01
1873-01-01
java.time.DateTimeException
JapaneseDate before Meiji 6 is not supported
明治5年12月31日

こちらも難しいことは特に必要なさそうです。


最後に別の型からの変換

final LocalDate ldm = LocalDate.of(1873, 1, 1);
System.out.println(JapaneseDate.from(ldm).format(formatter));
try {
  final LocalDate ldk = LocalDate.of(1872, 12, 31);
  System.out.println(JapaneseDate.from(ldk).format(formatter));
} catch (Exception e) {
  System.out.println(e.getClass().getCanonicalName());
  System.out.println(e.getMessage());
  System.out.println("明治5年12月31日");
}

結果

明治6年1月1日
java.time.DateTimeException
JapaneseDate before Meiji 6 is not supported
明治5年12月31日

最後に古の java.util.Date からの変換ですが、かなり面倒です。もう java.util.Date 使うなと言いたくなるくらいです。

final Date date = new Date();
final Instant instant = date.toInstant();
final ZonedDateTime zonedDateTime = instant.atZone(tokyo);
final JapaneseDate japaneseDate = JapaneseDate.from(zonedDateTime);
System.out.println(japaneseDate.format(formatter));

結果

平成29年3月23日

API的には JapaneseDate.from(instant) とやりたくなりますが、ゾーン情報を持たない Instant は変換できません(。逆もまた然り)。まあ、日付の計算も面倒ですし、 java.util.Date 使うのやめましょう。


java.util.Date 使うのやめましょう」と書くと、「これだからJava8の使えるサーバーの人間は…」と言われるので、紹介しておくと、これと同じAPIAndroidでも使えます。threetenbpというプロジェクトがあって、そちらがターゲットバージョン1.6で同じAPI(パッケージは異なる)のものを提供しています。したがってJava8が使えない環境の方はそちらを使うとよいです。

github.com

また、ThreeTenABP(Three Ten Android Back Port)というプロジェクトもあるようですが、ソースを見たところクラスの数が異様に少ないので同じAPIが使えるかどうかわかりません。どうやらメモリおよび速さの関係からバイナリファイルの中にゾーンの情報をすべて入れているようなのですが、JapaneseEraとかどうやってるんだろうという印象を持ったりします。

github.com

(追記:2017/03/24 1:03)教えてもらいました。ThreeTenABPはThreeTenに依存しているので同じAPIを使えるようです。


おわり

JetBrains系IDEと各種ITSとGitを連携してスマートに作業する

f:id:mike_neck:20170312130612j:plain

JetBrainsユーザーグループにて「IntelliJとYouTrack」というタイトルにて発表してきましたが、グダグダな発表をしたのでちゃんとまとめた記事を書きます。

jbugj.connpass.com


要点

JetBrains系IDEでは、標準に付属しているツールを用いることで、スマートに作業ができます。(なお、一部のIDEについてはリポジトリーからインストールしないといけなさそうです(GoglandにはTaskManagementプラグインが付属していないようだった))

  • Task Management プラグイン
    • ITS(イシュー管理システム)とIDEを連携させてTaskContext を管理できるプラグイン
      • Task - イシュー管理システムのチケットに該当する、作業の単位をあらわす
      • Context - 開いているファイルのカーソルの位置や、プロジェクトツリーの開いているディレクトリー、ツールウィンドウの状態などIDEの状態
  • VCSツール
    • IDEVCSを連携させる機能(以下Gitベースでの解説)
      • 標準的に使っている機能は普通に使える(add/commit/push/fetch/merge/rebase/checkout/log/diffなど)
      • Changelist
        • 変更を管理するための論理的なまとまりでファイル単位でコミットに加えるべきファイルを管理できる
  • JetBrains IDEを用いたワークフロー
    1. ITSに登録したチケットを元に Task を開く
    2. Task に関係のない変更(ちょっと気になった部分のリファクタリングなど)を行った場合は別の Changelist で管理する
    3. 他の Task に取り掛かる場合には開いているファイルなどの情報(Context)が自動で切り替わる
    4. Task 終了と同時にイシュー管理システムのステータスを更新する

Task Managementプラグイン

ITS(イシュー管理システム)とIDEを連携させてTaskContext を管理できるプラグインです。Task はイシュー管理システムのチケットに該当する、作業の単位をあらわします。Context は開いているファイルのカーソルの位置や、プロジェクトツリーの開いているディレクトリー、ツールウィンドウの状態などIDEの状態をあらわします。IDEにておこなう作業(Task)はITSのチケットに基づいておこなわれ、作業している状態(Context)が作業に関連づいているような感じです。


ITSの設定

特に何も操作をしていなければ、IDE上では Default Task が選択されており、その ContextDefault Task に紐付いたコンテクストになっています。

f:id:mike_neck:20170311201833p:plain

上の画像は Default Task にて作業をおこなった後の状態です。画像右上のあたりにあるプルダウンが Default Task となっているのがわかるかと思います。

まず、ITSに登録されているチケットと連動させます。

Tools > Task & Contexts > Configure Servers... からITSのサーバー設定を行います。

f:id:mike_neck:20170311203336p:plain

f:id:mike_neck:20170311204202p:plain

リストの左下にある + ボタンから連携するサーバーの種類を選びます。JIRA/YouTrack/Redmine/Trackなど主要なITSはカバーしています(Backlogはありませんが…)。

f:id:mike_neck:20170311204851p:plain

サーバーを選択した後、ITSのURL、ログイン用ユーザー名、パスワード、チケット検索用のクエリを入力してサーバーの設定は完了です。


このプラグインの具体的な使い方は後述します。


VCSツール

IDEVCSを連携させる機能もあります。以下はGitベースでの解説ですが、他のVCSでも同様のことができ(ると思い)ます。

基本的な操作
  • add は自動的に行われます。IDE内でおこなった変更はすべて add された状態になります(正確には add されていないけど、IDEにて add 相当の状態として管理されている)。
    • IDE外でおこなった変更(例えばスクリプトで出力したファイルなど)は add されていないので、 + + A で明示的に add する必要があります。
  • commit + K からおこないます。コミットの前にコードインスペクションやリフォーマット、importのオプティマイズなどをおこなえます。
  • push + + K です。
  • ブランチの選択、新しいブランチの作成は右下のブランチのプルダウンからおこないます。
  • fetchVCS > Fetchmerge は右下のブランチからマージするブランチを選んでから Merge を選びます

f:id:mike_neck:20170311212340p:plain

上の画像は現在のブランチから新しいブランチを作成する場合

f:id:mike_neck:20170311212424p:plain

上の画像は origin/master ブランチを現在のブランチにマージする場合


Changelist

IDEVCS連携は普通すぎるのですが、changelistはかなり強力な機能です。この機能によりコミットをより綺麗なものにできます。changelistは変更されたファイルを意味のある単位にまとめた集まりのことです。IDEのコミットはこのchangelist単位で行われます。よく言われる git add . をしないでファイルを指定して git add していると思いますが、changelist機能はIDEでその慣例をサポートするための機能です。先程IDE上で行われた変更は add された状態になると記述しましたが、正確にはchangelistで管理された状態になります。そしてコミットの際にchangelistにあるファイルが add されてコミットされます。

changelistを使っていくと、よくある関係ない変更がコミットに紛れ込んでしまうようなケースでも、簡単にコミットを綺麗に保つことができます。例えば、現在のチケットとは関係のない部分のちょっとしたリファクタリングをおこなったとしても、その変更を別のchangelistに入れておくことでコミットの中に関係のない変更が紛れ込むのを抑制したりできます。

changelistは左下の方にあるVersion Controlツールのchangelistタブで見ることができます。

f:id:mike_neck:20170311214825p:plain

changelistにはアクティブなchangelistとアクティブでないchangelistの二つがあります。エディタスペースなどでおこなった変更はすべてアクティブchangelistに加わります。そしてコミットする場合もアクティブchangelistに入っているファイルがコミット対象になります。

現在行っている作業と関係のない変更はすべてアクティブでないchangelistに加えるとよいでしょう。changelist間のファイルはドラッグ&ドロップで移動できます。

アクティブchangelistの切り替えは、changelistを選んでから左のチェックボックスのようなボタンを押すことで切り替えられます。

f:id:mike_neck:20170311221806p:plain

左にある + ボタンから新しいchangelistを作ることができます。

f:id:mike_neck:20170311215021p:plain

ダイアログにはchangelistの名前を入力します。なお、コミットコメントを先に書いておきたい場合は、コメント欄に書いておくこともできます。


IDEを用いたワークフロー

以下、JetBrains系IDE(ここではIntelliJ IDEA)とITS(ここではYouTrack)とGit(およびGitHub)を用いたワークフローのサンプルケースを記述します。

1. チケットから Task を開く

現在、次のような未解決のチケットが自分に割り当てられているとします。

f:id:mike_neck:20170312084931p:plain

Default Task のコンテクストで Tools > Task & Contexts > Open Task(または + + N)から上記チケットの中から一つ選びます。

f:id:mike_neck:20170312092017p:plain

Open Task ダイアログにて次のように設定して、 Task を開きます。

  • Update issue state → In Progress に更新
  • 現在の Context をクリアする(別の Context を作ると同意)
  • Create branch → masterからチケットのIDと同じ名前のブランチを作成
  • Changelist → 新しいchangelistを作成

f:id:mike_neck:20170312093559p:plain

Task を開いた後のIDEの状態。 Context をクリアすると、今まで開いていたファイルなどがすべて閉じられた状態になります。

f:id:mike_neck:20170312095312p:plain

もちろん、ITSではチケットの状態が更新されています。

f:id:mike_neck:20170312095421p:plain

2. 作業をする

コードを書きます。おこなった変更はすべて先程作成したchangelistに入ります。

f:id:mike_neck:20170312110615p:plain

ここで Task と関係のない変更を行います。

f:id:mike_neck:20170312112823p:plain

この変更は同じコミットに加えたくないので、新しいchangelistを作成して、そちらに移します。

f:id:mike_neck:20170312113154p:plain

+ K からコミットします。

f:id:mike_neck:20170312115056p:plain

3. 他の Task を開く

作業の途中ですが、別の変更もしてしまったため、その変更に該当するチケットから Task を開きます。この際、ブランチ作成元のブランチは現在のブランチでなくmasterを指定しておきます。

f:id:mike_neck:20170312120638p:plain

f:id:mike_neck:20170312120727p:plain

新しい Task に切り替える際に、アクティブchangelistの中身が空の場合はchangelistを削除するか聞かれます。削除してしまってかまわない場合は「Remove」を、削除しない場合は「Cancel」を選びます。

f:id:mike_neck:20170312120922p:plain

先程、一時的にchangelistに加えておいた変更をドラッグ&ドロップで新しいchangelistに移動させます。

f:id:mike_neck:20170312121204p:plainf:id:mike_neck:20170312121215p:plain

この後にコミットをしておきます。

そして Tools > Task & Contexts > Switch Task( + + T)から最初に作った Task に切り替えます。

f:id:mike_neck:20170312122858p:plain f:id:mike_neck:20170312123004p:plain

4. Task を終了する

Tools > Task & Contexts > Close Active Task( + + W)から Task を終了します。

f:id:mike_neck:20170312124412p:plain

Task を終了する際にできることは次のことです。

  • ITSのステータスを更新する
  • まだコミットしていない変更をコミットする
  • (コミットした上で)変更をブランチの作成元にマージする

プルリクエストからマージしたいので、ここではITSのステータスを「Waiting Review」に更新するだけにとどめます。

f:id:mike_neck:20170312125005p:plain f:id:mike_neck:20170312130136p:plain

なお、 Task を終了しても、IDE上では特に変化があるわけではありません。というのも、レビューによってこの後に同じ Context にて作業をする可能性があるかもしれないからです(と理由を言い切ることもできない…)。

プルリクエストを作っていないので、IDEから作ります。特にキーマップを割り当てていないかぎりプルリクエストの作成はキーマップからはできないので、 + + A からコマンドを探してプルリクエストを選びます。

f:id:mike_neck:20170312125432p:plain

titleとdescriptionを入力して「OK」を押してプルリクエストを作成します。

f:id:mike_neck:20170312125832p:plain

これでプルリクエストが作成されます。

f:id:mike_neck:20170312125949p:plain

f:id:mike_neck:20170312130256p:plain


このような形で、JetBrains系のIDEとITSを連携させるとかなりスマートに作業ができます。

おわり

testcontainersで使い捨てのデータベースコンテナを用意してSpring Bootアプリケーションのテストをおこなう

テストを流したらデータベースを起動していなくて、テストが全部コケさせることがよくあり、悩んでましたが、 @making さんに testcontainers を教えてもらったので試してみました(経緯は若干違う)。

github.com

testcontainersはテスト時にのみ使う使い捨てのデータベースなどをテスト時だけにDockerを用いて起動するライブラリーです。コンテナの設定値などをプログラムで記述できるため、うまく使えば設定を誤っていたがためにテストが落ちるなどのトラブルを回避できるかもしれません。


概要

この記事で書く内容は次のとおり。

  • testcontainers をtest compileスコープで用いる
  • JUnit4の ClassRuleContainer オブジェクトにてコンテナを起動する
  • ClassRuleContainer から接続するデータベースの接続情報を取得する

依存性

プロジェクトの概要

サンプルのプロジェクトはSpringBootでRESTベースのTodoアプリです。このプロジェクトで利用しているデータベースはMySQLです。このプロジェクトのテストにtestcontainersを用います。

gradle

いつも通り、gradleの依存性指定ですが、mavenなどは適宜読み替えてください。

dependencies {
  compile     'mysql:myql-connector-java:6.0.5'

  testCompile 'org.testcontainers:testcontainers:1.1.9'
  testCompile 'org.testcontainers:mysql:1.1.9'

  // spring boot starterなど
}

コンテナの起動

早速コンテナを起動したいところですが、サポート用のクラスを2つ作っておきます。

一つ目はMySQLのコンテナのクラスを用意するクラスですが、データソースにコンテナの情報を設定します。Springの起動時に動作させたいので、 ApplicationContextInitializer を実装するクラスとして作成します。

import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.testcontainers.containers.MySQLContainer;

public class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

  private static final MySQLContainer MYSQL = new MySQLContainer("mysql:5.7");

  static MySQLContainer database() {
    return MYSQL;
  }

  @Override
  public void initialize(ConfigurableApplicationContext context) {
    // コンテナからデータベースの接続情報を得る
    EnvironmentTestUtils.addEnvironment(
        "test-containers",
        context.getEnvironment(),
        "spring.datasource.url=" + MYSQL.getJdbcUrl() +
            "?useUnicode=true&" +
            "connectionCollation=utf8_general_ci&" +
            "characterSetResults=utf8&" +
            "characterEncoding=utf-8",
        "spring.datasource.username=" + MYSQL.getUsername(),
        "spring.datasource.password=" + MYSQL.getPassword(),
        "spring.datasource.driver-class-name=" + MYSQL.getDriverClassName()
    );
  }
}

もう一つは後述の理由により、hibernateMySQL57InnoDBDialect を継承してテーブルの文字コードをutf8mb4にするdialectを作ります(これはDDLを別途用意していれば必要ない)。

import org.hibernate.dialect.MySQL57InnoDBDialect;

public class InnoDBDialect extends MySQL57InnoDBDialect {
  @Override
  public String getTableTypeString() {
    return super.getTableTypeString() + " DEFAULT CHARSET=utf8mb4 ";
  }
}

次にこのdialectを使うようにテスト用の application.properties(ファイル名は application-unit-test.properties) を準備します。

spring.profiles.active=unit-test
spring.jpa.show-sql=true
spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database-platform=MYSQL
spring.jpa.properties.hibernate.dialect=com.example.InnoDBDialect

テストクラスを用意します。先程用意した Initializer クラスで用意した MySQLContainer@ClassRule のフィールドで保持するだけです。

import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.testcontainers.containers.MySQLContainer;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = { App.class })
@WebAppConfiguration
@ContextConfiguration(initializers = { Initializer.class })
@ActiveProfiles({"unit-test"})
public class TodoControllerTest {

  // Containerを@ClassRuleにて受け取る
  @ClassRule
  public static MySQLContainer MY_SQL_CONTAINER = Initializer.database();

  private MockMvc mvc;

  @Autowired
  private TodoRepository repository;

  @Autowired
  private WebApplicationContext context;

  private List<Todo> todos;

  @Before
  public void setup() {
    mvc = webAppContextSetup(context).build();
    repository.deleteAllInBatch();
    final List<Todo> list = Arrays.stream(defaultTodoItems())
        .map(p -> new Todo(p.getTitle(), p.getDescription()))
        .collect(toList());
    todos = repository.save(list);
  }

  private MediaType contentType = new MediaType(
      MediaType.APPLICATION_JSON.getType(),
      MediaType.APPLICATION_JSON.getSubtype(),
      StandardCharsets.UTF_8);

  @Test
  public void found() throws Exception {
    mvc.perform(get("/todo/" + todos.get(0).getId()))
        .andExpect(status().isOk())
        .andExpect(content().contentType(contentType))
        .andExpect(jsonPath("$.title", is("Testcontainersのことを調べる")));
  }
}

テスト実行のイメージ

これを実行します。

起動した後はtestcontainersから妙に生々しいデバッグログが流れてきます。なお、初回起動時はコンテナのイメージを作るために時間がかかります(ローカルにDockerイメージがあっても時間がかかる)。またコンテナが起動してから、接続してテストが始まるまでにまた時間がかかります。

f:id:mike_neck:20170311021819p:plain

その後、おなじみのバナーが表示されテストが実行されます。

f:id:mike_neck:20170311022216p:plain

f:id:mike_neck:20170311022228p:plain


という感じで、僕のようなおっちょこちょいにはかなり便利に使えるライブラリーですが、次のような制限(?)があります。

1. MySQLの設定を変更できない

ポート番号をデフォルトの 3306 から別のものに変えてテストを並列で実行できるようにしたいところですが、上のサンプルで用いた MySQLContainer クラスでポート番号を 3306 固定で持っているため変更できません(自分で MySQLContainer を書き直せばできるかもしれない)。ついでですが、 データベーススキーマ名、ユーザー名、パスワード、ルートのパスワードも test で統一されていたりします。

という感じで MySQLContainer クラスはわりと雑な実装になっています。

ただ、デフォルトの MySQLContainer の型が MySQLContainer<SELF extends MySQLContainer<SELF>> となっているので、このクラスは継承して利用することを前提に作られているのかもしれません。

2. my.cnf を書き換えられない

DockerのMySQLイメージで my.cnf をカスタマイズしたい場合は、 my.cnf ファイルの置かれたディレクトリーを /etc/mysql/conf.d にマウントすることで実現できますが、testcontainersで試した所、コンテナが起動できずテストが落ちました。そのような理由でtestcontainersでMySQLを用いる場合、デフォルトの my.cnf でテストすることになり、日本語がもれなく文字化けします。

(2017/03/13 7:13 訂正) どうやら、この問題は僕の my.cnf の書き方がマズかったらしく、修正したら余裕で起動できた…


以上

サンプルのコードはこちら

github.com