Quarkus + Doma 2 + Lombok を使う

f:id:nemuzuka:20190714095600p:plain

Doma 2が好きなので Quarkus でも使いたいと思って、これ を参考にサンプル作ってたのですが、Lombok と組み合わせると build が通らなくてハマってました。 どうも、Lombok が生成するメソッドが見つからないと怒られる。 で、ここ で聞いてみたわけです。すると、

Lombokアノテーションプロセッサが動いていないようですね。

と、@nakamura_to さんからコメントもらいました。

これで勝つる!

参考コードはこちら

これからも Doma 2 使っていこう。

ソフトウェア品質を高める開発者テスト

「知識ゼロから学ぶソフトウェアテスト」の著者である高橋さんの本です。 「そうそう」「わかるわかる」と頷きながら読み進めて、あっという間に読み切ってしまいました。 私が気になったところと共感できたところを書き出していきます。

上流品質を向上させる

そもそも私は、「上流品質」という言葉、知りませんでした。 個人的には上流/下流工程とかいう響きがあまり好きではないのですが、開発初期でも品質向上を上げる努力をしましょうってことなんですかね。

プロジェクトのしわ寄せは大体コーディングの後のテストフェーズで起こります。 それまでは順調と報告を受けていたプロジェクトもだんだんと暗雲立ち込めてきます。 下手をすると保守フェーズになってもバグが見つかり、システム利用者に迷惑をかけ、信頼貯金がなくなっていく...。

ほんと日本のソフトウェア開発現場はヤバい。

この一言に尽きると思います。わざわざリリース期日に近付いてから手戻りリスクの高い作業をする、というのはリスクヘッジできてない開発組織だと思います。

上流品質を向上させるには

本書では

  • 要求仕様の明確化
  • クラスや関数構造をシンプルに保つ
  • 単体・統合テストの実行
  • レビューの実施

を実行すれば良いと書かれています。 当たり前のようにやっている開発組織もあると思いますが、私もこれである程度の品質は担保できると思っています。 間違っても「システムテストをちゃんとやる」みたいなことではないみたいです。

開発初期でもテストをしなければ、多くのバグを後半で潰すことになり、(間に合わない為)納期を優先することで、潰し切れないバグを残してしまうことになります。 バグを0にするのは難しいですが、システムを利用する上で致命的なバグを出さない為にはこの作業をしておいた方が良いと私も思います。

単体テストで大事なのは網羅率でなく期待値の確認

これは本当にそう思います。 テスト対象のメソッド呼び出しで Exception が起きなければ OK としているテストコードのなんと多いことか。 なんかよく分からんけど正常終了している確認にはなりますし、Exception を throw するような振る舞いに変わった時にデグレに気づくことができるので全く意味がないとは言いませんが、「これでテストはバッチリです!」とは言いたくないなぁと思います。

カバレッジはわかりやすいので目標値として設定しやすいですけど、カバレッジ100%だからと言って品質が高いかというと全然そんなことないです。 境界値や状態遷移で起こりうる入力値のパターンを100%網羅し、その期待値を確認する方が高品質に繋がると私も思っています。

最近のシステムは外部サービス呼び出しも多くあって、単体テスト実行のたびに API 叩くわけにも行かないこともあるでしょうから、その辺りの単体テストは mock することが多いのではないでしょうか。 その時の期待値として、「mock を呼び出したこと」だけでなく、「mock の引数に正しく値を渡しているか」も確認する必要があると思っています。 Java で言うと、Mockitoをよく使うのですが、ArgumentCaptor でテスト対象メソッドから mock に渡したパラメータも確認する必要があると言うことです。 外部サービス呼び出しとか RDBMS に永続化処理があるテストは mock に置き換えて引数をチェックするってことをよくやります。

レビューしよう

しましょう。 レビューは「本人が気づくことに重点をおくもの」だそうです。なので、指摘より「なぜこういうふうになっているの?」という問いかけ形式にすると気づきに繋がるようです。理由によってはコメントした人も気付けるかもしれませんしね。

システムテストの自動化

SIer あるあるですけど、システムテストを自動化するのは夢物語だと思っています。 キャプチャー・リプレイの自動化テストなんか...悪夢でしかないです。 仮にうまくいったとしても、スクリプトのメンテナンスコストがバカにならないと思います。 ちょっとの変更でテストが壊れて、それを放置して自動化しなくなる未来が見えます。

やっぱり単体テストで多くのバグを見つけることが重要

重大なバグをシステムテストで見つける方式はリスクしかないです。それですり抜ける奴があったら...大騒ぎになります。 システムテストはやはり今の時点では手動でやるしかなく、それを毎回人の手でやるってのは苦痛です。 単体テストは自動化しやすく、内部的な状態を再現させやすいので、それに対するテストケースの追加が容易にできます。 バグが出てその修正とテストをセットにしておけば、他の修正でデグレが起きても早い段階で気づけます。 デグレ検知の為に毎回手動でシステムテストをやるとしたら...私はその現場から抜けようと思いますね。

あとがき

薄くて高い!

と読者の方に怒られるみたいですが、そういう方はもっと難しそうな文献を私たちにわかりやすく教えて欲しいものです。 ここまでわかりやすく要約してくれる本、そうそうないと思うんですよね。

まとめ

品質を上げるには特効薬はないんですよね。 要求仕様だって具体的に書かなければテストに落とせないし、テストに落とせないってことは開発完了にならないってことです。それだけでリスクです。 開発したそばからテストも書いておけば、レビューの input にもなるし、デグレ検知もできるしカバレッジも見れるしで「進んでる感」を得られます。

自分の経験を押し付けているように受け取る人がいるようですが、私は解決パターンの引き出しとして良書だと思いました。 他人が経験したものを知ることで同じような失敗しなかったり、成功への近道ができるなんて最高じゃないですか。

あと、まだ売っているらしい。(最近は amazon と中古で売ってるのくらいしか見てないな)

f:id:nemuzuka:20210318182721p:plain

CircleCI実践入門

CircleCI 自体は前から使っていたのですが、知識を update せにゃいかん気になってきたので読みました。 まとまった状態で読めるのはありがたいです。

構成

本書は、なぜ CI / CD が必要か?から始まり、CircleCI の基本、GitHub との連携やジョブが失敗した時のデバッグ方法の説明等がなされています。基本的に CircleCI の機能の説明なのですが、CI / CD のインフラをオンプレ、SaaS どちらにするのかの判断材料として読んでも良いんじゃないかと思います。

気になったところ

CircleCI の機能自体も発見が多かったのですが、ワークフローとジョブの作り方の所が気になりました。

ジョブは分割すべし

最初の方はプロジェクトのコードも小さいので、1つのジョブに

  • 静的解析
  • テスト
  • docker image 生成

みたいな感じで step を連ねて記載していくと思いますが、規模が大きくなってくると、途中で失敗しまうことも多くなります。 長い時間をかけて失敗してしまった時も、リソースを使用した分だけコストが発生します。 再実行する時も最初から実行することになります。 その辺りの余計なコストを発生させないようにするために、それぞれジョブで分けて、実行するのはワークフローで制御する方がベストプラクティスなようです。

確かに規模が大きくなってくると、テストの実行時間を短縮したくなり、並列で実行することを考えます。 その時に1つのジョブになっていると、並列で実行したい内容と1つのプロセスで実行したい内容が混在してきます。 テストだけ並列数を上げて実行したいのに静的解析も並列でやっても意味ありませんし、「他の処理が終わるのを待つ時間」というのもコストになりますから、もったいないですよね。

ジョブ間のファイル共有も考えられていて、ストレージやキャッシュを使って共有できます。

ジョブを小さな責務で分けておくことで、1つのジョブを並列で実行することもできますし、異なるジョブを同時実行することもできます。トータルでかかる時間を減らしやすくなると思いました。これは気にしていこう。

Exit Code 137 問題

私は Java で SpringBoot 使ったアプリケーションのビルドに使うことが多いので、gradle 使うことが多いです。 よく目にするのが、OOM でジョブが失敗する、という奴です。

ジョブを動かすリソースクラスを良いものにすれば大体解決はするのですが、コストが跳ね上がります。 そのために、メモリチューニングする必要があるのですが、GRADLE_OPTS-Dorg.gradle.daemon=false -Dorg.gradle.workers.max=2 しましょうと書いてあるのに好感が持てました。確かにサポートページに書いてあるのですが、これだけでも本書を読む価値ありだと思います。

gradle のプラグインによっては、環境変数でなく設定の方でメモリ指定する奴もいるのでご注意くださいませ。

まとめ

CI / CD のインフラ部分を面倒見られる体制があるなら考えなくて良いかもしれませんが、そんな余裕ない or リソースを他のことに回したいのであれば SaaS の検討しても良いんじゃないかと思います。他の SaaS は正直使ったことないのですが、CircleCI は安定していてお勧めできると思います。

目下の悩みは、機能追加に伴ってテストの数も増えていって、CI 回すたびに時間(+SaaS のコスト)がかかる問題の折り合いをどうつけるかなのですが、誰か相談できる人おらんかな...。

Think CIVILITY 「礼儀正しさ」こそ最強の生存戦略である

生存していきたいので読みました。 心理的安全性にもつながってくると思います。気になったフレーズをまとめてみます。

無礼な職場では、半分の人がわざと手を抜く

無礼な態度を取られると気分も悪くなりますが、仕事にかける労力や質を意図的に減らすようです。モチベーション落ちるとやる気なくなりますからね。 職場環境は良くした方が結果がついてきやすいと思います。

ある言動が「無礼」かどうかは相手がどう感じたかで決まる

同じ言動であってもどう受け止めらるかは世代によって違います。 相手が配慮を欠く扱いを受けたと思えばそれはその人にとって無礼な仕打ちを受けたことになります。

理不尽な態度は人の思考力を奪う

周りで怒鳴り声とか聞こえると集中力無くなりますからね。最大限の力を発揮してもらいたいなら無礼な態度を容認しないことが大事ですね。

無礼な人が近くにいると、その影響を受ける

チームリーダーとその上司ががそんな感じで無礼な空気が伝搬している現場があったなぁ...というなんとも言えない気持ちになりました。 直接無礼なことをされなくても居心地はよくなくて、どうにかして早く抜けたい、としか思わなかったです。自分から何か提案しようとは全く思いませんでした。

礼節があるとこんなに良いことが!

仕事が得やすい

確かにそんな気はします。とは言え能力がないと仕事を切られちゃうので難しいところですが...。

良い結果を出す

会社・チーム的な観点からこれは重要ではないでしょうか。 礼節があると、誰かがミスをしても自分の意思で率先して行動を起こすようになり、メンバーの精神的な消耗が少なくなります。 失敗しても大事になるまで黙ってることが無くなり、対処する時間を得ることができるはずです。 礼節がないと、責任を押しつけあったり、前向きな対応ができにくい空気な気がします。

礼節で不利になることはない

礼節ある態度をとるだけでその人は良い人に見えます。 いつか自分が困った時も他の人からの助けを受けやすくなるでしょうし、オススメです。 他人を気遣う方が良い結果につながりやすいと思います。 自分の得のためだけに他人を利用する事は、いずれ愛想を尽かされるでしょうね。

まとめ

礼節は事だけじゃなく、普段の生活にも活かせるんじゃないかと思いました。一人で生きていくわけにもいかないので。 無礼な態度をとっていれば無礼な反応がくるでしょうし、礼節ある態度をとっていれば礼節ある反応が来ると思います。 生きていくのであれば無礼な反応されたくないですし、礼節ある態度の方がメリット多そうな気がします。

能力があればその組織で必要とされるでしょう。ですが、必要とされ続けるには礼節が必要になってくるのだと感じました。 これからの組織は、「能力がある」だけが重要でなく、組織の文化を汚染しそうな人間は能力が高くても排除する動きが来るかもしれません。 今所属している組織が未来永劫変わらずに存続する保証もないでしょうから、組織の中で礼節を持って振る舞うことを加味しながら過ごすと今後良い結果につながるかもしれませんね。

「無礼」はウィルス。周囲に感染させるモノなので距離を置いた方が良さそうです。

Micronaut + Doma2 連携(超簡易版)

f:id:nemuzuka:20200611200142p:plain

やっぱり RDBMS と連携したかったのでやってみました。

参考にしたのは

です。

build.gradle

plugins {
  ...
  id 'org.seasar.doma.compile' version '1.1.0'
}

dependencies {
  ...
  annotationProcessor "org.seasar.doma:doma-processor:2.37.0"
  implementation "org.seasar.doma:doma-core:2.37.0"
  implementation 'io.micronaut.flyway:micronaut-flyway'

  runtimeOnly 'io.micronaut.sql:micronaut-jdbc-hikari'
  runtimeOnly "com.h2database:h2:1.4.200"
  ...
}

Doma2 はアノテーションプロセッサで interface から Dao の実装クラスを自動的に作ってくれます。

コネクションプールには HikariCP を使い、データベースのマイグレーションツールとして flyway を使います。 RDBMS は H2 にしましたが、この辺りは適宜変えてください。

LocalTransactionManager 関連

ここ に書いてあるように、Config の実装クラスを定義して、@Singleton で登録します。

DomaConfigFactory

@Factory
public class DomaConfigFactory {

  @Singleton
  public LocalTransactionDataSource localTransactionDataSource(DataSource dataSource) {
    return new LocalTransactionDataSource(dataSource);
  }

  @Singleton
  public LocalTransactionManager localTransactionManager(LocalTransactionDataSource dataSource) {
    return new LocalTransactionManager(
        dataSource.getLocalTransaction(ConfigSupport.defaultJdbcLogger));
  }
}

LocalTransactionManager を @Factory 経由で登録します。

DomaConfig

@Singleton
@RequiredArgsConstructor
public class DomaConfig implements Config {

  private final LocalTransactionDataSource dataSource;

  private final LocalTransactionManager transactionManager; // 1

  @Override
  public DataSource getDataSource() {
    return dataSource;
  }

  @Override
  public Dialect getDialect() {
    return new H2Dialect();  // 2
  }

  @Override
  public TransactionManager getTransactionManager() {
    return transactionManager;
  }
}
  1. LocalTransactionManager は Factory で登録したインスタンスを Inject します
  2. プロパティからとるとか良い感じにしてください。今回は決め打ちにしました

Dao

@DaoConfig

@AnnotateWith(annotations = @Annotation(target = AnnotationTarget.CLASS, type = Singleton.class))
public @interface DaoConfig {}

TaskDao

@Dao
@DaoConfig // 1
public interface TaskDao {

  @Insert
  Result<TaskEntity> insert(TaskEntity taskEntity);

  @Select
  @Sql("SELECT * FROM tasks ORDER BY task_id")
  List<TaskEntity> selectAll();
}
  1. Dao の interface に @DaoConfig を付与することで、アノテーションプロセッサで生成する impl クラスに @Singleton を付与してくれます。

後は、いつものように Doma2 を使えば良いです。

接続情報

RDBMS と flyway 用の設定をします。

datasources:  #1
  default:
    url: 'jdbc:h2:mem:micronaut-sample;LOCK_TIMEOUT=10000;MODE=PostgreSQL'
    username: 'sa'
    password: ''
    driverClassName: 'org.h2.Driver'
    schema-generate: CREATE_DROP
    dialect: H2
flyway:
  datasources:
    default:
      locations: db/migration #2
  1. 接続情報を設定します。schema-generate はテストの時だけ設定します。
  2. flyway が読み込む sql ファイルの配置場所を指定します。

Dao のテスト

TaskDaoTest

@MicronautTest // 1
@Property(name = "flyway.datasources.default.locations", value = "db/migration,db_fixtures/minimum") // 2
class TaskDaoTest {
  @Inject private TaskDao sut; // 3

  @Inject private LocalTransactionManager transactionManager; // 4

  @Test
  void testInsert() {
    transactionManager.required( // 5
        () -> {
          // setup
          var task = new TaskEntity(UUID.randomUUID().toString(), "タスク名", "内容");

          // exercise
          var actual = sut.insert(task);

          // verify
          assertThat(actual.getCount()).isEqualTo(1);
          assertThat(actual.getEntity()).isEqualTo(task);
          assertThat(sut.selectAll()).hasSize(3);
        });
  }

  @Test
  void testSelectAll() {
    transactionManager.required(
        () -> {
          // exercise
          var actual = sut.selectAll();

          // verify
          assertThat(actual).hasSize(2);
          assertThat(actual.get(0).getTaskId()).isEqualTo("001-dummy");
          assertThat(actual.get(1).getTaskId()).isEqualTo("002-dummy");
        });
  }
}
  1. @MicronautTest をつけることで、Inject できるようにします
  2. テスト開始時に flyway で table 作成するだけでなく、fixture となるデータを sql で入れたかったので、プロパティ flyway.datasources.default.locations を上書きます
    • db_fixtures/minimum は test/resources ディレクトリに配置しています。
  3. Dao を Inject することでアノテーションプロセッサで生成した Dao の実装クラスが Inject されます。
  4. LocalTransactionManager を Inject します
  5. Inject した LocalTransactionManager を使用してテストします。Dao のテストはトランザクションをテストクラスで制御しています。参考

トランザクション

@Transactional

アノテーショントランザクションを制御できるようにします。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Around
@Type(TransactionInterceptor.class)  // 1
public @interface Transactional {}
  1. @Transactional がついている時はトランザクションを張るようにします

TransactionInterceptor

@Singleton
@RequiredArgsConstructor
public class TransactionInterceptor implements MethodInterceptor<Object, Object> {

  private final LocalTransactionManager transactionManager;

  @Override
  public Object intercept(MethodInvocationContext<Object, Object> context) {
    return transactionManager.required((Supplier<Object>) context::proceed); // 1
  }
}
  1. 今回は手を抜いて、propagation は REQUIRED だけにしました

TaskUseCase

@Singleton
@RequiredArgsConstructor
@Transactional  // 1
public class TaskUseCase {

  private final TaskRepository taskRepo;

  ...
}
  1. 今回は UseCase に @Transactional を付与しました
    • このメソッド呼び出しが正常終了すると commit し、Exception を throw すると rollback します

まとめ

micronaut で doma2 を使ってアクセスできるようになりました。 Micronaut Data に Doma2 を...とも思ったのですが、簡単にいかなそうなので小手先でできるようにしてみました。Java の ORM 何が流行ってるのかわからん。

若干やっつけ感はありますが、アノテーショントランザクション管理できるようになりました。 今回の差分は大体こんな感じです。

*1:超感謝です!

Micronaut 2.0.0 に version up する

f:id:nemuzuka:20200611200142p:plain

前回 までの奴は micronaut 1.3.6 でした。

先日 2.0.0 がリリースされたので、それに追従してみましょう。

Session Authentication

https://micronaut-projects.github.io/micronaut-security/latest/guide/#session

依存 jar が変わります。

annotationProcessor "io.micronaut.security:micronaut-security-annotations"
implementation "io.micronaut.security:micronaut-security-session"

AuthenticationProvider の Deprecated なメソッドが削除されたので消しておきます。

application.yml 上の設定も変わります。何となく追えると思います。

Thymeleaf

依存 jar が変わります。

implementation "io.micronaut.views:micronaut-views-thymeleaf"

Thymeleaf 自体の定義は不要になりました。

差分みた方が早いかも

https://github.com/nemuzuka/micronaut-sample/pull/5/files

まとめ

IntelliJ のキャッシュをクリアしないと BeanInstantiationException が出て、@Singleton で登録したインスタンス@Inject で注入できないエラーになってしまいました。その時は IDE や Gradle のキャッシュをクリアしてみると良いかもしれません。 こいつのせいだったかも。

メジャーバージョン UP でしたが、そんなに依存していなかったので比較的スムーズでした。凝ったことしてると辛いかもしれません。せっかくアプリを作るのだから、バージョン UP に追従し続けることで、長く使えると良いですね。

バージョアップでデグレったことを検知する為にも、テストを書いておいた方が良いですよ。

Micronaut ことはじめ - テストを書いて歩こう (5)

f:id:nemuzuka:20200611200142p:plain

前回 までで一般的な Web アプリケーションを作るのに必要な Controller 部分のサンプルが溜まってきたと思います。あとは UseCase とか Repository が残っていますが、こいつらは Micronaut 依存が少ないので興味がある方は引き続きやっていただければと思います*1。 今回はテスト周りをやってみたいと思います。

コードはこちら (本文のコードは色々端折ってるのでコードをご確認ください)

Form のテスト

ここでは、Web ブラウザからのリクエストを受ける class を Form と呼んでいます。 Bean Validation で validation を行うのであれば、少なくともアノテーション(と設定)を正しく付与していることをテストする必要があります。

Validator

テストクラスはこんな感じです。

@MicronautTest // 1
class TaskFormTest {

  @Inject Validator validator; // 2

  @Test
  @DisplayName("validate でエラーが起きないケース")
  void testValidate() {
    // setup
    var sut = TaskForm.builder().taskName("name_0001").content("content_001").build();

    // exercise
    var actual = validator.validate(sut);

    // verify
    assertThat(actual).isEmpty();
  }

  @Test
  @DisplayName("null を渡した時")
  void testValidate_NullValue() {
    // setup
    var sut = new TaskForm();

    // exercise
    var actual = validator.validate(sut); // 3

    // verify
    var messages =
        actual
            .stream()
            .map(value -> value.getPropertyPath() + ":" + value.getMessage())
            .collect(Collectors.toList());
    assertThat(messages).containsOnly("taskName:must not be null", "content:must not be null");
  }
}
  1. @MicronautTest をつけることで、@Singleton をつけた class をテスト側で @Inject して使用することができます
  2. Validator を Inject します
  3. エラーがあると Validator#validate の戻り値に設定するので verify します

@Inject が spring で言う @Autowired です*2。 Validation の他に必要なテストがあれば追加していきます。

Controller の UnitTest

UnitTest では 依存する UseCase 等は Mock に置き換えて、メソッド呼び出し時を観点としたテストを行います。 Mock ライブラリとしてMockito を使っています。

テストクラスはこんな感じです。

@ExtendWith(MockitoExtension.class)  // 1
class AddControllerTest {

  @Mock private TaskUseCase taskUseCase; // 2

  @Mock private ConstraintViolationMessageConverter constraintViolationMessageConverter;

  @Mock private HttpRequest<Map<String, Object>> request;

  @InjectMocks AddController sut; // 3

  @Test
  @DisplayName("addTask のテスト.")
  void testAddTask() {
    // setup
    doNothing().when(taskUseCase).createTask(any());
    var taskForm = TaskForm.builder().taskName("タスク名").content("内容").build();

    // exercise
    var actual = sut.addTask(taskForm);

    // verify
    assertThat(actual)
        .isInstanceOfSatisfying(
            NettyMutableHttpResponse.class,
            response -> {
              assertThat(response.getHeaders().get(HttpHeaders.LOCATION)).isEqualTo("/tasks");
              assertThat(response)
                  .returns(HttpStatus.MOVED_PERMANENTLY, NettyMutableHttpResponse::getStatus);
            });
    verify(taskUseCase).createTask(taskForm.toCreateTaskUseCaseRequest()); // 4
  }
}
  1. @MicronautTest の代わりに @ExtendWith をつけます
  2. テスト対象 class が Inject している class を @Mock で mock 化します
  3. @InjectMocks がついていると @Mock を付けた class を Inject してインスタンスを生成します
    • 今回は AddController のコンストラクタの引数に @Mock を付けた class のインスタンスを受け取るようにしています
  4. mock 使ったら呼び出し回数や引数の verify をしましょう。そうしないとテストの意味が激減します

アクセス権やログイン済みか?の状態のテストはここでは行いません。Java のロジックとして想定通りかの観点でテストを行います。Micronaut はあまり関係ないです。

IntegrationTest

画面が絡んでくるとどこまでやるべきか悩ましいです。 ひとまず雑に要素の存在チェックをすることにしました。E2E のテストは別でやると思ってます。

@MicronautTest // 1
class AddControllerIntegrationTest {

  @Inject IntegrationTestHelper integrationTestHelper; // 2

  @Inject TaskUseCase taskUseCase;

  @Test
  @DisplayName("GET /tasks/add のテスト")
  void testIndex() throws Exception {
    // setup
    var client = integrationTestHelper.login(); // 3
    var request = integrationTestHelper.buildGetRequest("/tasks/add");

    // exercise
    var response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

    // verify
    assertResponseWithBody(
        response,
        HttpStatus.OK,
        html -> {
          assertElementExists(html, "input[name='csrfToken']"); // jsoup でドキュメントの要素を verify
          assertTextEquals(html, "h3.title.is-3", "Task 登録");
        });
  }

  @Test
  @DisplayName("POST /tasks/add のテスト")
  void testAdd() throws Exception {
    assertThat(taskUseCase.allTask()).isEmpty();

    // setup
    var client = integrationTestHelper.login();
    var csrfToken = integrationTestHelper.getCsrfToken(client, "/tasks/add"); // 4

    var postParameter =
        "csrfToken=" + csrfToken + "&taskName=task_name_001&content=task_content_001";
    var request = integrationTestHelper.buildPostRequest("/tasks/add", postParameter);

    // exercise
    var response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

    // verify
    assertResponseWithLocation(
        response,
        HttpStatus.MOVED_PERMANENTLY,
        location -> assertThat(location).isEqualTo("/tasks"));

    // 永続化していること
    var actual = taskUseCase.allTask();
    assertThat(actual).hasSize(1);
    var actualTask = actual.get(0);
    assertThat(actualTask.getTaskId()).isNotNull();
    assertThat(actualTask)
        .returns("task_name_001", TaskUseCaseResult::getTaskName)
        .returns("task_content_001", TaskUseCaseResult::getContent);
  }
}
  1. @MicronautTest をつけます。netty が立ち上がります
  2. テスト用に使う Helper を用意しておきます
  3. login 状態にした client を取得してアクセスします
  4. CSRF トークンを取得してアクセスします

テスト用に起動した内部サーバとの通信を Running the Embedded Server のように

@Inject
@Client("/")
HttpClient client; 

でやりたかったのですが、今回のサンプルは Session Authentication を組み込んでいるので、ログイン済みの状態を示すために Cookie が必要でした。 なので、io.micronaut.http.client.HttpClient でなく、java.net.http.HttpClient を使用することにしました。 また、ログイン成功時の Cookie を保持した HttpClient や CSRF トークンを取得する処理を Helper として定義しています。

IntegrationTest ではブラウザからのリクエストを受けた時の振る舞いを確認します。 なので、

  • ログインはしているが、操作する権限を持っていないユーザがリクエストした
  • エンドポイント呼び出し時に RDBMS に登録しているか
  • リクエストパラメータの Validation でエラーになった時

等もこのテストでの確認ポイントになります。

例えば AWSSDK を mock して...と言う時は、Using Mockito Mocks を参考に Mock にすることができます*3。 テスト用に設定を変えたい時は @Property@MicronautTest の propertySources を指定すれば良さそうです。

まとめ

どこまでテスト書くかは時間との兼ね合いかもしれませんが、テストがあればデグレしてない安心感を得られます。 フレームワーク依存の部分もテストがあれば version up する時に気付きやすくもなります。面倒かもだけどテスト大事です。

こうしてみると Spring でできたことができないケースは少ないと思います。 テストの起動も Spring よりは早い印象を受けます。ちょっとした API サーバはもちろんですが、Micronaut で大体いける気がしてきました。

*1:私が Repository に使うなら doma2 ですかね

*2:spring でも @Inject 使えるんですね...

*3:ちょっと面倒ですけどね