小ネタ) SpockのData Driven Testingでテスト観点をUnrollのメソッド名に記述するとよいかもしれないと思った話

Spockのデータ駆動テスト(Data Driven Testing)では、メソッド名としてデータ変数を含んだ文字列を使うことで、テスト結果を見た時にどこで失敗したか、わかりやすくすることができます。

データ駆動テスト — Spock 1.0-SNAPSHOT

さて、この時に利用するデータ変数は、テストで利用するパラメータと期待値を使うことが基本だと思います(実際ほとんどのケースではそれで十分だと思いますが)。

しかし、イテレーション(テストのパターン数)が増えてきたり、複雑なロジックのテストの場合、パラメータと期待値だけでは、そのパターンのテスト理由・意図が一見してわからないこともあります。その時、テスト観点も含めると便利だと思ったので、紹介します。

デフォルト

そもそもデータ変数をメソッド名として使わない場合、例えば、こんな感じのテストがあった時、

    @Unroll
    def "maximum of two numbers"() {
        expect:
        max(a, b) == ans

        where:
        a  | b   || ans
        3  | 4   || 4
        -3 | -4  || -3
        10 | 10  || 10
        10 | -20 || 10
        0  | 5   || 5
        -5 | 0   || 0
    }

個々のメソッド名はイテレーションのインデックスだけで区別されます。

f:id:jappy:20170114073900p:plain

パラメータと期待値

メソッド名を次のように変えると、

    def "maximum of two numbers (#a, #b) -> #ans"() {

どのケースで失敗したかが一目瞭然になります。

f:id:jappy:20170114074230p:plain

(このキャプチャではすべてpassしていますが)

テスト観点を加える

今度は、以下のようにしました。

    @Unroll
    def "maximum of two numbers (#a, #b) -> #ans (#description)"() {
        expect:
        max(a, b) == ans

        where:
        description << [
                'positive & positive',
                'negative & negative',
                'same',
                'positive & negative',
                'zero & positive',
                'zero & negative'
        ]

        a  | b   || ans
        3  | 4   || 4
        -3 | -4  || -3
        10 | 10  || 10
        10 | -20 || 10
        0  | 5   || 5
        -5 | 0   || 0
    }

ポイントは2つで、

  • データ変数として、テスト内で直接使うわけではないが、テスト観点・意図を表す description を追加する
  • テスト観点は長くなりがちなので、データテーブルの可読性を下げないように、データテーブルの列ではなく << オペレータで列挙する

実行結果は想像がつくと思いますが、以下のようになります。

f:id:jappy:20170114075047p:plain

おわりに

上の例だと単純過ぎてあまりメリットが見えにくいですが、実際に業務でテストコードをレビューしていてこんなことがありました。

  • テスト対象のロジックが複雑(パラメータの組み合わせ)なため、データテーブル中にコメントを書いて、テスト目的を分かるようにしていた。
    (コメントに書くくらいなら、テストメソッド名になっていた方が、CIでのレポート結果などでもすぐに分かるので、有用だと思います)
  • パラメータや期待値が複数の項目から成るリストで、データ変数としてそのまま展開するとものすごい長いメソッド名になってしまい、テスト結果の一覧性を悪くしていた。

なので、その辺りをちょっとだけ改善するために、テストの意図を表す文字列をメソッド名に含めるとよいのでは?と思ったのでした。

マルチデータソースにおけるGradleでのFlywayタスク実行

マルチデータソース(2つのRDB)を使っている場合に、FlywayのGradle PluginでflywayMigrateなどのタスクを実行し、両方のDBにマイグレーションを実行したい場合。

flyway {
    user = 'user1'
    password = 'pass1'
    url = 'jdbc:xxx://host1/db1'
}

def flyway2 = new org.flywaydb.core.Flyway()
flyway2.setDataSource('jdbc:xxx://host2/db2', 'user2', 'pass2')
flyway2.setLocations('filesystem:./src/main/resources/db_migration2')

tasks['flywayMigrate'].doLast {
    flyway2.migrate()
}

// 他タスク(flywayClean, flywayInfo, ...)も同様

プラグインの機能としては提供されていないので、Gradle(Groovy)側のカスタマイズで対応してみました。

Spring Sessionのバージョンアップとデシリアライズエラー

Spring Security + Spring Sessionを使っている場合、通常、セッションの中身はシリアライズされてセッションストアに保存されます(実際に確認したのはRedisのみ)。

シリアライズされる org.springframework.security.core.context.SecurityContextImpl には serialVersionUID が定義されているのですが、その値は SpringSecurityCoreVersion の中にあります。
https://github.com/spring-projects/spring-security/blob/4.1.x/core/src/main/java/org/springframework/security/core/SpringSecurityCoreVersion.java#L35-L41

コードの通り、Spring Securityのバージョンごとに決まった値になります。

したがって、Spring Securityをバージョンアップした場合、それまで使っていたセッションがリクエストされると、デシリアライズに失敗します。
issueがGitHubやstarckoverflowに挙がってます。

シリアライズに失敗した場合、サーバ側エラーとなって500エラーを返してしまうので、本来はセッションの有効期限切れと見なして403エラー辺りを返したいところです。

対象方法についてのメモです。

対処1: いったんセッションの中身をクリアする

乱暴ではありますが、ソースコメントにも「Classes are not intended to be serializable between different versions.」とある通り、シンプルに、バージョンアップの際にセッションストア側をクリアしてしまうのが確実だと思います。

Redisの場合は、キーが spring:session:* にマッチするものを削除すればOKです。

対処2: 独自の SessionRepository を作ってデフォルトの挙動をカスタマイズする

以下に紹介されていました。

sdqali.in

シリアライズエラーをcatchして、該当のセッションをRedisから削除しています。
また、getSessionnull を返すことで、有効なセッションが見つからなかった時と同じ振る舞いにさせているっぽいです。

方法1と比較すると、追加のコードが必要ではあるものの、バージョンアップの度にRedisの中身をメンテする必要はなくなるので、保守性では優れている気がします。

対処3: セッションの中身をマイグレートする

方法1と方法2も、バージョンアップ前のセッションを破棄するアプローチでした。
Spring Sessionのバージョンアップの頻度はそう高くはないとは思いますが、依存ライブラリのバージョンアップというサーバ側の都合で、セッションが破棄されてしまうのが許容できないケースもあるかもしれません。

そういうったケースの対応方法を考えてみました。一つのプログラム内で新旧の SecurityContextImpl が共存できないので、2つのステップ(プログラム)に分けています。

  1. 既存のセッションの中身をエクスポートする。エクスポートする際は旧バージョンのSpring Securityを利用してデシリアライズし、 Javaシリアライズに依存しない形 (例えばJSONなど)で出力する。
  2. エクスポートした内容を読み取り、新バージョンのSpring Securityを利用してセッションストアに戻す。

うーん、無理やりな感じが否めないので、オススメはできそうにないですね。

正解が見えない課題を圧倒的に解決する超仮説思考 #書籍

正解が見えない課題を圧倒的に解決する 超仮説思考

正解が見えない課題を圧倒的に解決する 超仮説思考

印象に残ったところを自分なりにまとめるとこんな感じでした。

  • 5F, 3C, SWOT等のフレームワークは有用ではあるものの、枠にあてはめてしまうが故に、多様的・立体的である現実の事象に対して見落としや乖離があり、的外れな解を導き得る。
  • すぐに思いつく最初の10個のアイディアは捨て、ひねり出した11個目以降のアイディアを採用する。
  • 自分の見えないところに答えがある。視点を移し、想像力を磨いていく。
  • 前提条件はいつか変わる、変えられる。
  • 実践(仮説検証)を繰り返していく。うまくいかなかった時は、仮説が間違っていたことよりも、仮説はあっていたがどこかにボトルネックがあって見落としているという可能性を疑う。

Spring BootでJettyのログを少しカスタマイズする

Spring Bootでリクエストログを出力するための方法については、以下の記事で紹介されています。

qiita.com

以下の内容はSpring Boot 1.4.3で確認してます。また、Jetty以外のアプリケーションサーバの場合は当然ながら全く違った設定方法になります。

X-Forwardedヘッダを解釈させる

ELB(AWS)やHerokuなど、前段でSSLが終端されてから(SSL Termination)後続のSpring Bootアプリがリクエストを処理するようなケースでは、本来の送信元IPをアプリが知るためにX-Forwarded-For他、X-Forwarded系のヘッダを解釈させるようにする必要があります。

JettyではForwardedRequestCustomizerというクラスが提供されているので、それを追加してあげればよいです。

    // 途中略

    for (Connector connector : server.getConnectors()) {
        if (connector instanceof ServerConnector) {
            HttpConnectionFactory connectionFactory = connector.getConnectionFactory(HttpConnectionFactory.class);
            connectionFactory.getHttpConfiguration().addCustomizer(new ForwardedRequestCustomizer());
        }
    }
}

出力されるログに独自項目を追加する

元々用意されている NCSARequestLog を継承し、 logExtended メソッドを上書きした拡張ログクラスを作ることで、元の出力内容に加えて独自の項目を出力することができます。
例えばAuthorizationヘッダの内容を最後の項目に出力させたい場合は、以下のような感じです。

public class NCSARequestLogExt extends NCSARequestLog {

    @Override
    protected void logExtended(StringBuilder b, Request request, Response response) throws IOException {
        super.logExtended(b, request, response);

        String authToken = request.getHeader("Authorization");

        if (authToken == null) {
            b.append(" \"-\" ");
        } else {
            b.append(" \"");
            b.append(authToken);
            b.append("\" ");
        }
    }
}

Cloud First Architecture 設計ガイド #書籍

気が付いたら2017年も1週間が過ぎてしまいました。まさか7月のイベントが2016年の最後のエントリになるとは思いませんでした。

年々顕著にエントリが減ってきているので、今年は週1くらいのペースで書く習慣をつけていきないなと思います。
ネタが少ない時なんかは、読んだ本の簡単な感想で、エントリ数だけは積んでいきたいと思います(読書ログでいいじゃん、というツッコミはあるんですが…)

すぐに読めちゃうボリュームの割には広く浅く書かれていて、復習になりました。
AWSにおける具体的なアーキテクチャ例、みたいなものは書かれておらず、アーキテクチャ設計の考え方を知るための本、という印象です。 Salesforceの名も所々出てきます。 第5章「アーキテクチャー設計ガイド」はまた読み返したいと思います。

Doma勉強会 in 東京に行ってきた #doma_tokyo

Doma勉強会に行ってきました。

eventdots.jp

最近はJPAを使っていてDomaを使えていないのですが、以前そこそこ長い間(3年以上)Doma(1, 2両方)を使ったシステム開発をしてたので、そのあたりからの進化とか色々知ることができました。

スライドは公開されていますので、個人的に知らなかっこと、気付きなどについて超簡単なメモ。

知らなかったこと

  • SQL中のIN句で利用されるバインド変数のリストが空でも、 IN (NULL) となり、SQLエラーにならない
    • これは地味ながら嬉しいアップデート。
  • 複数カラムをグループ化するための仕組みとしてエンベッダブルクラスが利用できる。
    • 色々使い道がありそうな気がしました。結合した結果を受け取るエンティティクラスを簡潔に定義するのに使えたりしないかなー、とか。Domaだと結合したクエリの結果ごとにエンティティクラスが必要なので、プロジェクトで使ってた時はエンティティクラスのフィールド定義が重複しがちだった記憶があります(2テーブルなら継承でもよいのですが)
  • IntelliJ IDEAでも使える → JetBrains Plugin Repository :: Doma Support
    • だいぶ前の情報ですが、Eclipseでしか使えないという古い情報のままだったので…
  • Domaと直接関係のないJavaの話ですが、 Stream#map(...).collect(toList())Stream#collect(mapping(..., toList())) とも書ける。

実践的な活用方法として得たこと

  • ドメインクラスをうまく活用すると、かなりのことを型検査でき、余計なミスを早期につぶせる
  • Streamを返す検索は、クローズを呼び出し側の責任で行わないとならないので、避けた方が無難そう
  • collect検索を利用し、1件検索、複数検索で同じSQLファイルを使うとよい
  • エンティティのイミュータブル性を確保する
  • 複数データソースを使う場合のレイアウト。やっぱりパッケージレベルで分けた方がメンテナンスしやすいっぽい

感想

  • 中村さんの発表 Domaの開発で大切にしている10のこと - Qiita に共感できるところが多くて、やっぱりDomaは使いやすく、開発者に優しいツールだなと改めて思いました。
  • 当初作る予定だった機能が2010年に完成していながらも、今に至るまで開発を継続していけるのは純粋にすごいと思いました。個人的にはJava8対応したバージョンがかなり早い時期にリリースされたのが印象的だった記憶があります。
  • doma-spring-bootもあるし、JPAにあれこれハマるよりは、Domaに移行するのもちょっと検討したい。
  • DOMAのロゴがN◯SAに見えたのは私だけじゃなかったようでした。

色々と勉強になりました。ありがとうございました。