烏滑稽

ブログはじめました(迫真)

Kafkaアプリケーションのユニットテスト

10日0時を回ってしまった気がしますが Distributed Computing Advent Calendar 12/9 分、書いていきます。寝なければセーフ!
Kafkaアプリケーションのユニットテストについてですが具体的な方法ではなく概論のような形になります。

qiita.com

Kafkaアプリケーション is 何

今回話すKafkaアプリケーションは、Kafka brokerそのものの上で動くなにかのプログラムではなく、
Kafka brokerに対してconsumeしたりproduceしたりするクライアントアプリケーションのことです。
例えば

  • Kafkaから読んだデータをフィルタリングしたり情報を付加した後に別トピックに書き戻すアプリケーション(ETL)
  • Kafkaから読んだデータを集約して結果をKVSなどに書き出すアプリケーション
  • Kafkaから読んだデータを使って異常検知し、何らかのAPIを叩くアプリケーション
  • StormやKafka Streamsのアプリケーション

Kafkaの利用目的がストリーム処理であれデータバスであれ、 クライアントアプリケーションで何かを実現したいからKafkaを使うわけで、基本的にはこれらの開発をすることになります。

Kafkaアプリケーションのテスト

Kafkaアプリケーションのテストはどうやって書いたらいいでしょうか。
パッと思いつくところだとKafkaとの結合部以外の処理(アプリケーションのビジネスロジック)を部品ごとに単体テストするというものです。例えばフィルタクラスの単体テストであったり、Stormだと単純にビジネスロジックだけ抜き出すのは難しいのでmockito等のモックを使うかもしれません。

さて単体テストを書きました。

「アプリケーションのテストなんだからこれで充分。Kafka Consumer APIやStormは公式のライブラリなんだから結合部はリリース前に結合環境にデプロイして受け入れテストすればOK。やったぜ。」

となるでしょうか。
Kafkaアプリケーションの開発をやられている方は感じるところがあるんじゃないかと思うんですが、
このままだと結合環境にデプロイしてもまず間違いなく思った通りに動きません。
開発者としては「フィルタの単体テストなんかよりよっぽどKafkaとの結合部の方が自信がない」ということがままあります。

悲劇の例
  • 開発者「アプリ書いたぜ。早く結合環境でちゃんと動くか試したいゾ。プルリクレビューして、どうぞ。」
  • レビューワー「うーん、たぶん動くんじゃないですかね(適当)(マージポチー)」
  • Jenkinsさん「単体テスト成功やね。パッケージングしてリポジトリに上げときますね。」
  • 開発者「パッケージングできたから早速テストサーバにデプロイするゾ。他のサーバにKafkaも既にインストールしたンゴ。(アプリケーション起動ポチー)」
  • JVM「クソみたいな結果」
  • 開発者「あっ(絶望)。プルリクからやり直しですね、自信ないけど原因と思われるコード修正しました。」
  • レビューワー「zzz」

まるでレビューワーが昼行燈の如く書きましたが、これはレビューワーは悪くないです。
試行錯誤の最中にプルリクを送ってもレビューワーに細かい意図は伝わりにくいものです。開発者自身がよくわかっていないものをレビューさせるのはそもそも酷な話です。
レビューワーの興味はアプリケーションの仕様とそれが本当に仕様通り動くかというところで、言い換えればテストコードが見たいはずです。メインコードの微修正はあまり見たくないでしょう。

誰しもアプリケーションの一連の結果を確認しながら開発したい

実際には「悲劇の例」で書いたようなバカ正直なことはあまりされないと思います。
ローカルで開発したテストコードをリモートでたてたKafkaに対して実行して(つまりテストコードにリモートホスト名を書いている)、結果を見ながら開発することの方が多いのではないかと思います。

これはかなり悪くない方法ですがそれでも課題があります。

  • リモートでテストKafkaをたてるのがそもそもめんどい
  • そこでやった試行錯誤やテストコードはプルリクするときに消すことになる(リモートホスト名や接続設定が書いてあるテストコードを残すわけにはいかない)
    • 開発したコードに自信をもったのは今の自分だけ。レビューワーや、自分でも後から見て正しく動くか疑わしい

ローカルKafkaユニットテスト

前述の2つの課題はローカルでコードからテストKafkaを立てることができれば解決します。

  • コードだけで立ち上がるならあんまりめんどくない
  • ローカルで自動的に立ち上がるならそのテストコードはコミットに残すことができる

そのやり方はKafka公式ドキュメントのFAQの最後に載ってます!
FAQ - Apache Kafka - Apache Software Foundation

スニペットを見ると若干長いです。
パッと見何をやってるかわかりづらく、これアプリケーション毎に毎回書くの?っていう気持ちになるかもしれません。
やりたいことはZooKeeperとKafka立ち上げだけなのだからもっとこう2, 3行でなんとかならないのっていう気がします。
またマルチテナントの自動ビルド環境にこのままだと不適です。Kafkaのポートが固定されているために、指定したポートが自動ビルド環境で空いてなかったらエラーで落ちます。

これらの問題を解決するために便利なラッパライブラリを各人(チーム)で開発しているかもしれません。
かくいう私も開発しているのですが、今回はそれではなくて↓を紹介します。
GitHub - chbatey/kafka-unit

個人の方が開発されているようで、ライセンスはApache 2.0となっています。
こちらよくできていまして、2行書けばZooKeeper, Kafkaのローカル起動ができて、1行でシャットダウンできます。
各ポートも空きポートと固定指定両方サポートしています。
他手動トピック作成やお手軽produce, consume機能も付いています。

単純なETLテストフロー例

Aトピックからconsumeした文字列の最後に!をつけてBトピックに投げるアプリケーションをテストする場合

  • ZooKeeper, Kafkaを起動する
  • Aトピックにサンプルデータを投げる
  • ちょっとウェイト
  • Bトピックをconsumeしてサンプルデータに!がついてるかassertする

この場合アプリケーションのconsumer設定 auto.offset.reset=earliest にすることに注意!
デフォルトのlatestだと最新からメッセージを読むので何もメッセージが流れてこないように見えます。
これはKafka FAQのconsumer欄の一番目に書いてあるやつですがKafkaアプリ開発を始めた人はほぼ確実にハマります。まだハマっていない人は頭の片隅に入れておきましょう。

Stormアプリの開発をするんだけど

ローカルKafkaとStorm Local Clusterを使って必ずユニットテストしましょう!
Stormのテストコードを書くのは調べてみるとかなりめんどいと思われるかもしれません。が、ほとんどの場合毎回結合環境でテストをするコストがユニットテストの初期コストを上回ります。
もちろんユニットテストが通っても結合環境で落ちることはままありますが(サンプルデータの想定が違っているなど)、とは言えユニットテストを通せばかなり洗練した状態で結合テストに臨めます。
例えばStormでやりがちなシリアライザブルでないオブジェクトの配布エラーはユニットテストの時点で気付くことができます。