Spring BootアプリのテストをSpockで書く(続編)
以前にSpring BootアプリケーションのテストをSpockで書く方法を紹介しましたが、この方法ではテストの所要時間が長くなる問題がありました。本稿では他の方法を紹介します。
具体的には、インナークラスの @TestConfiguration
でMockを定義するとSpecificationクラスごとにApplication Contextが再生成されてしまうため、スローテストの原因になる問題がありました。
// Specificationのインナークラス @TestConfiguration static class MockConfig { final detachedMockFactory = new DetachedMockFactory() @Bean ExternalApiClient externalApiClient() { detachedMockFactory.Mock(ExternalApiClient) } }
特定のテストケースだけBeanをMockに置換したい場合はこの方法が必要ですが、多くの場合は必要ないはずです。その前にプロダクトコードの設計を見直す方がよいでしょう。
コンポーネントテスト
コンポーネントテストのレベルでは、コンストラクタでMockを注入する方法で十分です。
// プロダクトコード @Component class BarService { final ExternalApiClient client BarService(ExternalApiClient client) { this.client = client assert client } }
// テストコード @SpringBootTest(webEnvironment = NONE) class BarServiceSpec extends Specification { @Subject BarService service ExternalApiClient client = Mock() def setup() { service = new BarService(client) } }
Mockではなく本物のBeanが必要な場合は、Specificationクラスに @Autowired
なプロパティを追加してBeanを取得します。
@SpringBootTest(webEnvironment = NONE) class BarServiceSpec extends Specification { @Subject BarService service ExternalApiClient client = Mock() // Mock Bean @Autowired HelloProvider provider // 本物のBean def setup() { service = new BarService(client, provider) } }
E2Eテスト
冒頭で述べた方法ではSpecificationごとにMockを定義していましたが、全テストケースで共通のMockを定義するとApplication Contextが再利用されるので所要時間が大幅に短くなります。
// Mock定義 @Configuration class IntegrationTestConfiguration { private final detachedMockFactory = new DetachedMockFactory() @Bean ExternalApiClient externalApiClient() { detachedMockFactory.Mock(ExternalApiClient) } }
// テストコード @SpringBootTest(webEnvironment = RANDOM_PORT) class BarControllerSpec extends Specification { @Autowired TestRestTemplate restTemplate @Autowired ExternalApiClient client }
@Primary
を付けるなどの工夫をすれば、この方法とSpecificationごとにMockを定義する方法を組み合わせて利用できると思います。(未検証…)
まとめ
これまでに以下の方法を紹介しました。
- SpecificationクラスごとにMockを定義する方法
- Specificationクラスごとに本物とMockを使い分けられる。
- 個別にApplication Contextが生成されるので、スローテストの原因になる。
- コンストラクタでMockを注入する方法
- Application Contextが再利用されるので、テストの所要時間が短い。
- テストコードでコンストラクタ呼び出しを記述するのが面倒。
- 全テストケースで共通のMockを定義する方法
- Application Contextが再利用されるので、テストの所要時間が短い。
- Mock定義が共通なので融通が利かない。
参考までにGitHubにサンプルプロジェクトを置いています。
Spring BootでログやActuatorにバージョン情報を含める
ログやActuatorにバージョン情報を含めておくと、本番環境でどのバージョンのアプリケーションが実行されているか簡単に確認できるので便利です。
ビルド時にapplication.ymlにバージョン情報を含める
Gradleでは、以下のようなビルドスクリプトを書くとapplication.ymlの文字列を置換できます。
version = System.getenv('TAG_NAME') ?: 'SNAPSHOT' processResources { filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [ 'APP_NAME': project.name, 'APP_VERSION': project.version ]) }
ここでは、ビルド時に TAG_NAME
という環境変数にバージョン番号が設定されている前提で、application.ymlを置換しています。バージョン番号でなくてもコミットハッシュや日時、ビルド番号など何でも構いません。
例えば、application.ymlに以下を書くとアプリケーション名やバージョン番号に置換されます。
app: name: "@APP_NAME@" version: "@APP_VERSION@"
なお、Spring Bootの公式ドキュメントではSimpleTemplateEngineによる置換が紹介されていますが、Placeholderと競合するので使いづらいです。ReplaceTokensを利用する方がおすすめです。
72. Properties & configuration
ログにバージョン情報を含める
Spring Cloud Sleuthを使用している場合は、以下を設定するとログの全行にバージョン情報が付加されます。
spring: application: name: "@APP_NAME@-@APP_VERSION@"
2016-02-26 11:15:47.561 INFO [example-1.0.0,2485ec27856c56f4,2485ec27856c56f4,true] 68058 --- [nio-8081-exec-1] com.example.Application : Hello
(追記) spring.application.nameはService Discoveryのキーに使われるので、上記は避けた方がよいです。
@making 確かに。バージョンも入れたい場合は自分でMDCでカスタマイズする方がよさそうですね。
— いわてぃ (@int128) 2017年2月16日
起動時に出るだけでよいという場合は、適当なプロパティにバージョン情報を入れてログで出力するとよいでしょう。
@Slf4j @SpringBootApplication class App { static void main(String[] args) { def context = SpringApplication.run(App, args) log.info('Started {}', context.environment.getProperty('info.app.name')) } }
Actuatorでバージョン情報を返す
Actuatorを使用している場合は、以下のように設定するとREST APIでバージョン情報を取得できるようになります。
info: app: name: "@APP_NAME@" version: "@APP_VERSION@"
GET /management/info
にアクセスすると以下のようなJSONが得られます。
{ "app": { "name": "example", "version": "1.0.0" } }
Actuatorでバージョン情報が取れると便利なのでぜひ使ってみてください。
ターミナルのウィンドウタイトルにホスト名などを入れる
久しぶりにzshネタです。
複数のタブを開いていろんなサーバにSSHしていると区別が付かなくなってきたので、ウィンドウタイトルに実行コマンド、ホスト名、カレントディレクトリを入れてみました。zshの組み込みコマンドだけで実現してみました。
function _window_title_cmd () { local pwd="${PWD/~HOME/~}" print -n "\e]0;" print -n "${pwd##*/} (${HOST%%.*})" print -n "\a" } function _window_title_exec () { local pwd="${PWD/~HOME/~}" print -n "\e]0;" print -n "${1%% *}:${pwd##*/} (${HOST%%.*})" print -n "\a" } [[ "$TERM" =~ "^xterm" ]] && { add-zsh-hook precmd _window_title_cmd add-zsh-hook preexec _window_title_exec }
コマンドプロンプトのカスタマイズは下記のエントリで紹介しています。ご参考まで。