GeekFactory

int128.hatenablog.com

Spring BootアプリのテストをSpockで書く(続編)

以前にSpring BootアプリケーションのテストをSpockで書く方法を紹介しましたが、この方法ではテストの所要時間が長くなる問題がありました。本稿では他の方法を紹介します。

int128.hatenablog.com

具体的には、インナークラスの @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にサンプルプロジェクトを置いています。

github.com

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のキーに使われるので、上記は避けた方がよいです。

起動時に出るだけでよいという場合は、適当なプロパティにバージョン情報を入れてログで出力するとよいでしょう。

@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
}

コマンドプロンプトのカスタマイズは下記のエントリで紹介しています。ご参考まで。

int128.hatenablog.com