Magnolia Tech

techっぽいことを書くブログです

Specs2でテストに失敗したときに例外を投げる

Scala用の代表的なテスティングフレームワークの1つがSpecs2です。

"Acceptance specification"と呼ばれる自由度の高い仕様の記述方法が特徴的です。

以下のコードはDeepThrought. calcUltimateQuestionメソッドの結果の正当性を確認するテストコードです。

import org.specs2._

object DeepThrought {
  def calcUltimateQuestion: Int = {
    // Because this calculation requires strictly 7.5 million years,
    // I will write the answer first :)
    42
  }
}

class UltimateAnswerSpec extends Specification { def is = s2"""
the Answer to the Ultimate Question of Life, the Universe, and Everything $checkCalc
"""

  def checkCalc = {
    val answer = DeepThrought.calcUltimateQuestion
    answer must be_==(42)
  }
}

def is = s2"""..."""で囲まれた中に仕様を自由に記述し、テストを実行するメソッドのメソッド名を$を付けて埋め込むと、テストが実行された時に結果が埋め込まれ、成否が表示されます(上記の例でいえば、caclメソッド)。

> test
...
[info] Done compiling.
[info] UltimateAnswerSpec+ the Answer to the Ultimate Question of Life, the Universe, and Everything
[info] Total for specification UltimateAnswerSpec
[info] Finished in 64 ms
[info] 1 example, 0 failure, 0 error
[info] Passed: Total 1, Failed 0, Errors 0, Passed 1
[success] Total time: 2 s, completed Dec 5, 2017 9:41:10 PM

テストクラスの名称の後ろに「+」が表示されていれば、そのテストクラスのテストは成功したことになります。

試しにテストコードの数値を「43」に書き換えると、当然演算結果が一致しないのでテストは失敗します。

[error] x the Answer to the Ultimate Question of Life, the Universe, and Everything
[error]  42 != 43 (HelloSpec.scala:17)
[info] UltimateAnswerSpec

実際のテストの判定は、マッチャーと呼ばれるメソッドで行います。上記の例では、テストメソッドの最後にある「answer must be_==(42)」がマッチャーを使った箇所になります。このコードでは、計算結果が格納されたanswerという変数が42でなければならないことを表しています。

判定結果はそのままメソッドの返り値として使われ、テストの成否が判定されます(Scalaではメソッドの最後に書かれた値が返り値になります)。

ここまでは通常のspecs2の使い方です。

よくある(?)ミス

これは自分だけかもしれませんが、specs2を初めて使ったとき、あと久しぶりに使った時によくやる間違いが、テストメソッドの中でマッチャーを複数使うことです。

極端な例ですが、下記のテストは、テストメソッド全体としては成功してしまいます。

answer must be_>(43)
answer must be_==(42)

なぜならば、あくまでspecs2ではテストメソッドの戻り値だけがテストの成否に使われるからです。

PerlのTest::Moreなどに慣れていると、うっかり書いてしまいますね。

対応策1: andで連結する

こんな時はマッチャーの結果をandで連結します(上記の例を成功するように書き換えています)。

answer must be_<(43) and be_>(41)

テスト対象の変数が複数有るときはコードブロックで連結します。

    {
      question must be_==("How many roads must a man walk down?")
    } and {
      answer must be_==(42)
    }

対応策2: ThrownExpectations traitを使う

ThrownExpectations traitを使うと、マッチャーが失敗すると、例外を送出し、テストが失敗します。

class UltimateAnswerSpec extends Specification with org.specs2.matcher.ThrownExpectations { def is = s2"""
the Answer to the Ultimate Question of Life, the Universe, and Everything $checkCalc
"""

  def checkCalc = {
    val answer = DeepThrought.calcUltimateQuestion

    answer must be_>(43)
    answer must be_==(42)
  }
}

結果はこうなります。

[error] x the Answer to the Ultimate Question of Life, the Universe, and Everything
[error]  42 is less than 43 (UltimateAnswerSpec.scala:22)

おわりに

うっかり書いてしまうテストメソッド内の複数マッチャーに対する対策を書きました。

ThrownExpectationsは、なんとspecs2の作者で有る@etorreborre‏さんに教えてもらいました。感謝!

基礎からわかる Scala

Scalaスケーラブルプログラミング第3版

yet anotherなsbtランナー「sbt-extras」を使う

Scalaのビルドツールと言えばsbtですね。

インストール自体は簡単で、事前にJavaの実行環境がインストールされていれば、各種パッケージ管理ツールでインストールすれば使えます。

sbt Reference Manual — Installing sbt

Homebrewの例でいけば、以下のコマンドでインストールされます。

$ brew install sbt

sbtコマンド自体はただのランナーで、実態は一緒にインストールされているsbt-launch.jarというjarを起動しているだけです(JVMですからね)。

実行したカレントディレクトリに、project/build.propertiesが有れば、そのバージョンに合わせたsbtの本体(jar)がダウンロードされ、なければインストールされているsbt-launch.jarで指定されているsbtがダウンロードされます。

sbt-extras

状況によっては必ずしもプロジェクトディレクトリの外には何もインストールしたくない場合、つまりsbtコマンドすらインストールしたくないことも有るかと思います(チームで環境を統一させたいけど、いちいちインストールさせるのはハードル高いとか、サンプルコードを配布したいときとか)。

そんな時にはyet anotherなsbtランチャーであるsbt-extrasが便利です。

sbt-extrasシェルスクリプト一枚で提供されていて、プロジェクトディレクトリに指定したsbt-launch.jarをダウンロードするところから始めてくれるので、あらかじめsbtを環境にインストールしておく必要がありません。

github.com

セットアップと、実行

READMEにはインストール方法として下記のように書かれていますが、これでは(ホームディレクトリ配下ですが)グローバルにインストールするのと変わりません。

$ curl -Ls https://git.io/sbt > ~/bin/sbt && chmod 0755 ~/bin/sbt

今回はプロジェクトディレクトリを作って、そこにダウンロードするようにします(project-dirは適当にリネームしてください)。

$ mkdir [project-dir]
$ curl -Ls https://git.io/sbt > [project-dir]/sbt && chmod 0755 [project-dir]/sbt
$ cd [project-dir]
$ mkdir project
$ echo sbt.version=1.0.4 > project/build.properties
$ ./sbt

2017/11/30時点でのsbtの最新バージョンは1.0.4なので、それを指定しましたが、原則最新バージョンを指定しておけば良いでしょう。

sbtコマンドを実行した時点で必要なjarが全て自動的にダウンロードされ、指定したバージョンのsbtが起動します。

あとは適宜build.sbtを作って、src以下にコードをどんどん書いていきましょう。

これでJavaの実行環境さえインストールされている環境であれば、プロジェクトディレクトリのリポジトリgit cloneするだけで、誰でもsbtが使えるようになります(.gitignoreを作ってtarget/以下がリポジトリに含まれないように気をつけましょう)。

そのほかの使い方

$ ./sbt -h

上記のコマンドを叩くとヘルプが表示されます。JVMオプションの追加や、sbtのバージョンの指定などもできます。

注意事項

sbt-launch.jarのダウンロードパスがsbtのバージョンによって変わったりして、sbt-extrasも日々バージョンアップしています。そのため、古いsbt-extrasではsbt 1.0.xがダウンロードできなかったりと言った事象も有りました。使うsbtをアップデートするときはバンドルしているsbt-extrasの対応状況を必ず確認するようにしましょう。

「テスト駆動開発」を購入し、Scalaで写経を始めた

最近話題の「テスト駆動開発」を購入した。

テスト駆動開発

テスト駆動開発

第一部はJava + Junitで書かれているが、さすがにそのままJavaで書き写しても面白くないので、Scala + ScalaTestで書き直した。

最近欠かさず聞いているajito.fmではgoで写経する話が出ていた。

ajito.fm

ちなみに本を読む前に、和田卓人さんがゲストで出演している第13回は一通り聞いておいた方が、この本へ接する姿勢が明確になって非常に良いで、ぜひ聞いて欲しい。

とりあえず2日間ほどかけて第一部の第1章〜第11章までを、Scalaで実装してみた。第12章から機能追加が始まるので、一端ここで区切り。

メンバ変数のスコープ制御の考え方の違いとか、case classを使えばequalsの実装が不要になるとか、scalatestのassertマクロの種類とか、意外と引っかかるポイントが有って、思ったより先に進まなかった。

だけど、一度実装した物を、成功するテストを元にガシガシ書き換えていく感じ(最後には、最初に作ったはずのクラスが2つとも無くなってしまう)は、けっこう新鮮だった。

全然15年前の出版であることを感じさせないのは、きっとこれが根源的な行為だからなんだろうな。

とはいえ、一人でこれを継続するのはなかなか厳しそうなので、複数人・複数言語・ファシリテータ付きで一日かけて、あーでもない、こーでもないと言いながら進めていくのが良さそう。

最初は凄く回りくどく感じるかもしれないけど、「歩幅の調整」というキーワードで詳しく解説されているし、前述のajito.fmでもその話が出てくる。

とりあえずテストコードを書いたことが無い人は、一度第一部だけでもいいので、Java以外の言語で写経しながら進めると良いかも。

Scalatra 2.6がリリースされました

Scala用のWAFであるScalatraのver 2.6がリリースされました。

最近まであまり活発にメンテナンスされていなかったのですが、GitBucketがScalatraベースということで、GitBucketの作者であるtakezoeさんの手によって非常に活発なメンテナンスが継続されるようになりました。

今回はtakezoeさんがメインとなってからの初の大型リリースです。

変更点は多岐に渡るので、リリースノートを参照して下さい。

今回は新機能のリリースというより、未来の大型バージョンアップに向けて古くなった機能をdeprecated指定したり、モジュール名を直したりといった改修が主な点です。

Scalatra 2.6.0 is out - Scalatra

また、プロジェクトのひな形もアップデートされていますので、以下のコマンドでver 2.6ベースのプロジェクトをすぐに始めることができるようになっています。

$ sbt new scalatra/scalatra.g8

自分もプラグインだとか、ドキュメント、サンプルコード周りで色々とコントリビュートできたので、ぜひ皆さん使ってみて下さい!

Scalaスケーラブルプログラミング第3版

Scalaスケーラブルプログラミング第3版

Scalatra in Action

Scalatra in Action

Haconiwaの話を聞いてきた

connpass.com

なかなか参加できない平日昼間のイベントに行ってきました。

github.com

1時間枠の中で、そもそもコンテナとは?コンテナの仕組みは?Haconiwaの特徴は?といったことがHaconiwaの作者であるudzuraさんから流れるように説明されて行って、最近気になってたことが一気に理解できました。

特にchrootとか、cgroupあたりの解説が聞けたのが良かったです。

なぜmrubyなのか?といった質問があり、crubyでは最初からマルチスレッドで動いていてシステムコールに制約があるけど、mrubyではシングルスレッドでスタートするので、その問題が無い、という回答が「なるほどー」という感じでした。

なお、冒頭で「綴りの正答率50%」という発言が有りましたが、自分も思いっきり間違えてツイートしてしまいました。これからは正しい方に入ります!

HTTP responseのstatus-line、reason-phraseの内容にどこまでこだわるか?

scalatraのメンテナンスでservletのdeprecatedなメソッドを使っている箇所を直そうと思ったら単純には行かず、延々とHTTP protocolのstatus-lineに入るreason-phraseについて調べることになりました。

せっかくなので、その結果をまとめておきます。

status-lineのreason-phraseとは?

まずはstatus-lineのreason-pheraseについておさらいします。

おなじみHTTP protocolのresponseの1行目はstatus-lineと呼ばれ、「HTTP/1.1 200 OK」や、「HTTP/1.1 404 Not Found」といった内容がサーバサイトの処理結果に応じてクライアントに返されます。

このstatus-lineの内容はRFC 7230で以下のように定義されています。

status-line = HTTP-version SP status-code SP reason-phrase CRLF

status-codeは3桁のコードで、HTTP requestの成否が返されます。その一覧はRFC7231で確認することができます。

続くreason-phraseには人間が理解できるようにstatus-codeの内容を簡潔に示すテキストが入ります。RFC 7230では以下のように定義されていて、あまり現代では必要性無さそうです。

The reason-phrase element exists for the sole purpose of providing a textual description associated with the numeric status code, mostly out of deference to earlier Internet application protocols that were more frequently used with interactive text clients.

更には続けてクライアントソフトは無視すべきであると書かれています。

A client SHOULD ignore the reason-phrase content.

reason-phraseの形式は先ほどのRFC 7230で以下のように定義されています。

reason-phrase  = *( HTAB / SP / VCHAR / obs-text )

先頭がアスタリスクなので、全て省略しても規格上は正しい、つまりreason-phraseは規格上省略可です。

一応RFC7231にstatus-codeごとにreason-phraseの推奨案が記載されていますが、あくまで推奨なので独自に変えたとしてもプロトコルの動作に影響を与えないこととなっています(それで挙動が変わることが有れば実装の方が誤り、という事です)。

サーバアプリケーションでのreason-phraseの設定処理

普段サーバアプリケーションを書いていてreason-phraseのことをいちいち気にすることは無いと思いますが、これは一般的にアプリケーションサーバ側でよしなに処理してくれるからです。

いくつか実装を見てみましょう。

Sinatra + WEBrick(Ruby - Rack)

Sinatra + WEBrickはご存じの通り、Rackというインタフェースを通じて接続されています。Rackのインタフェース上、status-codeしか渡せないので、Sinatraではreason-phraseを設定していませんが、WEBrick側でreason-phraseを設定してくれます。

https://github.com/ruby/webrick/blob/43fc65e0619f033d02e8f4a0d647687f20939240/lib/webrick/httpresponse.rb#L125-L128

ちなみにRackの中でもreason-phraseが定義されていますが、WEBrickでは独自に定義されています。

https://github.com/rack/rack/blob/6b942ff543416e0c82196f0790d4915c7eead4cb/lib/rack/utils.rb#L487-L552

Jetty(Java - Servlet)

JavaServlet、例えばJetty実装では、以下のように定義されています。

https://github.com/eclipse/jetty.project/blob/6fd3351272ba949dc80d160c18f0e403757a0c0d/jetty-http/src/main/java/org/eclipse/jetty/http/HttpStatus.java#L27-L33

Servletにはエラーレスポンスを即時に返すsendErrorというメソッドが有りますが、以下のようにreason-phraseを取得しています。

https://github.com/eclipse/jetty.project/blob/829fa4fe9bc650e336e7412aae6e6bffa9b37778/jetty-server/src/main/java/org/eclipse/jetty/server/Response.java#L635-L641

なお、Servletのもう一つの実装、Apache Tomcatではver 8.5以降でreason-phraseはデフォルトでは送信されなくなっていて、オプションで送信するように変更可能なものの、その設定自体は非推奨となっています。また、Tomcat 9からはこのオプションすら削除されるそうです。

60362 – Missing reason phrase in response

独自のreason-phraseを設定する

冒頭で説明した通り、reason-phraseは規格上は推奨案は有っても厳格な定義はされていません。例えば先ほどのServletSendErrorメソッドの第二引数に任意の文字列を渡せば独自のreason-phraseを設定することができ、規格上も問題ありません。

RackやPSGI(Perl)と言った言語でのインタフェースではstatus-codeしか渡さないので、アプリ側から独自のreason-phraseを定義する方法は無さそうです(serverの実装しだい…でも見たことが無い…)。

HTTP/2

最近普及してきたHTTP/2では規格自体にreason-phraseが無いそうです。

github.com

Scalatraのメンテで遭遇した事象

古いServletの規格では2つの引数を取るsetStatusというメソッドが用意されていて、status-codeと独自のreason-phraseを設定し、かつbody部を定義することができました。現在最新のScalatra 2.5.1ではこれが使われています(reason-phraseが設定されていない時は、status-codeのみをセットするsetStatusが使われるが、ユーザーコード側からは見えないように隠蔽されている)。

現在(といってもServlet 2.1以降)では2つの引数を取るsetStatusは非推奨になり、独自のreason-phraseを設定したい場合は、body部無しにただしにエラーを返す(エラー例外を送出し、エラーログに出力される)SendErrorメソッドを使うしか有りません。

現在のServletではエラーが発生した時に、body無しで簡潔にエラー内容を伝えたい時(ログに残したい時)に使う思想のようです。しかし先ほどのTomcatのように、そもそもreason-phraseを一切送信しない実装も有るので、必ずクライアント側に送信される保証は無く、あまり使い道は無さそうです。

この解決はGitHubのissueで!

結論

というわけで、ここまで見てきて…

  • 設定内容がそもそも規格上保証されていない(RFC 7231)
  • アプリ側で自由に設定できない規格/実装がある(Rackは設定できない、Servletはエラー時のみ設定可)
  • 送信されない実装が有る(apache Tomcatなど)
  • HTTP/2では規格上存在しない
  • そもそも規格でクライアントは無視すべきとまで書かれている(RFC 7230)

現代では「status-lineのreason-phraseにこだわってはいけないこと」がよく分かりました。HTTP responseの死活チェックなどで「200 OK」をチェックしないように気をつけましょう!

SRE サイトリライアビリティエンジニアリングを読んだ

ちょっと前に邦訳が出て話題になっていた「SRE サイトリライアビリティエンジニアリング」をようやく読んだ。

GoogleエンジニアによるSRE(Site Reliability Engineering)の解説記事の集合体。全体のページ数は500ページを超えていてかなりのボリュームだけど、割と短い記事の集合なので拾い読みには丁度良いかも。

日本ではメルカリがSREチームを大々的にアピールしていますね(既に日本、と限定できない組織になっていますが)。

tech.mercari.com

トイル(toil)

色々と読み所の多い本では有りますが、個人的には第5章「トイルの撲滅」が一番興味深く読めました。

トイル(toil)という言葉は今まで知らなかったのですが、辞書を引くと「骨折り(仕事); 苦労」と書かれていて、あまり良い意味では無さそうです。

本書の中では、以下のように定義されています。

プロダクションサービスを動作させることに関係する作業で、手作業で繰り返し行われ、自動化することが可能であり、戦術的で長期的な価値を持たず、作業量がサービスの成長に比例するといった傾向を持つものです。

システムを運用していれば日々色々なアラートメッセージを受け取りますが、必ずしも全てに新規性が有るわけではなく、日々既知のアラートを受け取り、「(改善を図る暇も無いし)既知だから問題ありません」とインシデントをクローズすることが有るかと思います。

しかし、このような作業の割合が増えていくと、本書でも触れられているようにエンジニアとしてのキャリアの停滞につながります。

たいてい一つ一つの作業はすぐに終わるし、システムは安定的に運用されているし…と言った理由でこのような小さな問題はマネージメントサイドから注目されることが少ないのですが、実はこうゆうことを一つ一つクリアにしていくことが大事なんだな、と改めて理解しました。

トイル、減らしていきましょう!

おわりに

SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム

SRE サイトリライアビリティエンジニアリング ―Googleの信頼性を支えるエンジニアリングチーム

なお、原著がオンラインで無料公開されているので、まずはそちらを読んでみると良い。

Google - Site Reliability Engineering