Magnolia Tech

いつもコードのことばかり考えている人のために。

Test2::Prettyを作るために調べたことと、やったこと

Perl 5.26.xでTest2がコアモジュール化しました

Perl 5.26.xでは、コアモジュールのテスティングフレームワークであるTest::Moreモジュールに大改修が行われ、完全にゼロから書き直されれたTest2モジュールベースのものに入れ替えられています(version 1.3xx以降がTest2ベースです)。

Test2そのものの機能説明や、コアモジュール化の状況は、YAPC::Fukuokaでakiymさんが発表された「新時代のテストフレームワークTest2」というスライドをご参照ください。

akiym.hateblo.jp

Test2では、それまでのTAP(Test Anything Protocol)べったりの内部構造から、テストを1つの「イベント」として捉え、テストイベントのストリームを処理する、という思想に変わっています(事実、初期はTest::Streamという名前で開発が始められていました)。おなじみのTAP形式での出力も、単なるフォーマッタによる1つの表現形式に過ぎません。

コアモジュール化により失ったもの

しかし、一方でTest::Moreに対するモンキーパッチで作られていた特殊なテストモジュールが動作しなくなる、という弊害も有ります。執念のような互換性テストにより、たいていのメジャーなテストモジュールはTest2上で動作しますが、根本的にどうしようもないものも有ります。

その中でも最も有名なのが、tokuhiromさん作のTest::Prettyです。

$ cpanm Test::Pretty
--> Working on Test::Pretty
Fetching http://www.cpan.org/authors/id/T/TO/TOKUHIROM/Test-Pretty-0.32.tar.gz ... OK
Configuring Test-Pretty-0.32 ... OK
Building and testing Test-Pretty-0.32 ... FAIL
! Installing Test::Pretty failed. See /xxxx/xxxxxx/.cpanm/work/1521365294.14600/build.log for details. Retry with --force to force install it.

残念ながらテストが通らずインストールが失敗します。これはTest::MoreのコアであるTest::Builderモジュールを徹底的に置き換えるというTest::Prettyの構造がさすがにTest2ベースのTest::Builderでは対応できなかったためです。

Test2::Formatterモジュール

Test2にはTest2::Formatterという、発生したテストイベントを実際のテスト結果の出力へ変換する仕組みが用意されています。例えば、TAP形式への変換はTest2::Formatter::TAPというモジュールが用意されています。

というわけで、Test::Prettyの出力を再現するTest2::Formatter::Prettyを作ってあげれば良い、ということですね。

まずは最小のTest2::Formatter::Prettyを用意してみましょう。Test::Formatterを継承したモジュールは最低限これだけ有れば動きます。

package Test2::Formatter::Pretty;

use strict;
use warnings;

our $VERSION = 'v0.0.1';

use Test2::Util::HashBase qw{
    no_numbers
};

use parent qw/Test2::Formatter/;

sub hide_buffered { 1 }

sub write {
    my ($self, $e, $num, $f) = @_;

    print "にゃーん\n";
}

1;
use Test::More;

use strict;
use warnings;

pass("success!!");
fail("failure!!");

done_testing;
$ T2_FORMATTER='Pretty' perl -Ilib test01.t
にゃーん
にゃーん
にゃーん
にゃーん
にゃーん
$ perl test01.t
ok 1 - success!!
not ok 2 - failure!!
#   Failed test 'failure!!'
#   at test01.t line 7.
1..2
# Looks like you failed 1 test of 2.

テストが2つ(成功と失敗)、ダイアログメッセージが2つ(テストの失敗の詳細と、テストの失敗数)、テストプラン(1..2)が出力されていることが分かるでしょう。

つまり、writeメソッドの中でテストイベントに応じて出力する内容をひたすら変換し、標準出力か、標準エラー出力へ編集していけばOKということになります。

Test::Prettyの仕様を振り返る

ではここで改めてTest::Prettyの仕様を振り返ってみます。

  • テストが成功すれば「ok」ではなく、「✓」を緑で表示する -> 見やすい
  • テストが失敗すれば「not ok」ではなく、「✖」を赤で表示する -> 見やすい
  • テストをスキップすれば、「skip」を黄色で表示する -> 見やすい
  • サブテスト自体のテスト成否を表示しない -> サブテストの中身を見れば充分なので、表示が余計
  • テスト名を宣言しないと、テストコードの該当行のソースをテスト名として表示する -> 実は意外とこれで充分

環境変数HARNESS_ACTIVEが有効なときの挙動はまた違うんですが、複雑になるので、ここでは割愛します。

Test2::Formatter::Prettyを実装する

というわけで、できました「Test2::Pretty」モジュールです。

github.com

はっきり言ってTest2::Formatter::TAPと、Test::Prettyからのコピペのキメラなので、詳しくはソースコードを見てください、としか言い様はありませんが、Test2モジュールがイベントモデルとして綺麗に整理されているので、実装量は非常に少ないです。一部サブテスト周りで表示をスキップするために汚いコードが有りますが、順次改善していきたいと思いますので、使ってみてどんどん感想をください。issue、PRも歓迎です。落ち着いたら、cpanにアップします。

おわりに

長年ウォッチしていたTest2がついにコアモジュール化して、感慨深いものが有ります。 ただ、最近すっかり追いかけていなくて、今回久しぶりにコードを読んでみると全然別物のようになっていたので、追いかけるのに非常に苦労したし、意外とドキュメントも未整備なところが有ったので、これからまたコントリビュートしたい、と思いました。

Test2::Pretty、ぜひ使ってみて下さい。

初めてのPerl 第7版

初めてのPerl 第7版

続・初めてのPerl 改訂第2版

続・初めてのPerl 改訂第2版

「UNIXという考え方」を読み返している

プログラミングPerlには、「プログラマの三大美徳」という言葉が出てくる。

色々なところで散々言及されているので、ここで深く解説することはしないけど、ちょっと検索しても比較的新しい日付でこんなブログ記事が出てくる。

moneyforward.com

だからといって、Perlという言語がこの三原則と対応付いた機能はコレです、とか言っているわけではない。あくまで考え方というか、思想として書かれている。

UNIXという考え方」という古典的名著が有って、こちらはもうすこし直接的に、UNIXというオペレーティングシステムが作られてきた背景というか、機能の裏にある考え方がまとめられたもの。とはいっても、UNIXが開発される前にこんな10箇条が張り出されて、「この方針で作る」なんてものでもなく、歴史として振り返ったときに、こんなことが言えるよねっていうことがまとめられている。

だからこそ逆に学びが多いのかもしれない。

個人的には「1つのプログラムは1つのことをうまくやらせる」「90パーセントの解を目指す」あたりが非常に心に突き刺さっていて、やっぱり余計な完璧さを求めるのは良くないというか、先回りして考えたことはたいていロクなことにならないと思っている。

また、3.4章と3.5章の見出しがもう素晴らしい言葉の連続で、考えさせられる。

第二のシステムは、委員会が設計する 第三のシステムは第二のシステムで「火傷」した人が作る

UNIXという考え方」は素晴らしいことがたくさん書かれていて、かつ書籍自体はそれほどのページ数が無い(わずか148ページ)。ちょっとした移動時間の隙間でもさらっと読める量になっている。一方で前述した「プログラミングPerl」はVol.1だけでも756ページも有って、とても移動時間に読める量ではないし、そもそもプログラミング言語の解説書だ。

できれば、こんなプログラミング言語オペレーティングシステム、ライブラリの設計思想を普遍的な内容にまとめて解説した本が有ればいいのに。

UNIXという考え方―その設計思想と哲学

UNIXという考え方―その設計思想と哲学

プログラミングPerl〈VOLUME1〉

プログラミングPerl〈VOLUME1〉

ServletのrequestオブジェクトのgetParameterメソッドを使うとinputStreamが空っぽになっている

https://javaee.github.io/servlet-spec/downloads/servlet-3.1/Final/servlet-3_1-final.pdf

上記Servletの仕様書のセクション3.1.1に書かれている通りなんですけどね。

  • POST method
  • content typeがapplication/x-www-form-urlencoded
  • requestオブジェクトのgetParameter系のメソッドを呼び出す

という条件が揃うと、servletコンテナ側でinputStreamが消費されてしまうので、アプリケーション側で読み込むことができない、とのこと。

読み込み用のメソッドを使っただけでオブジェクトの状態が変わるのって…という感じですが、アプリケーション側からするとraw dataの状態で検査したいことも有ったりして、おせっかいな機能ですね。

ほかにもbody部のパラメータと、URLのパラメータを勝手に1つのMapにミックスしたりと、「それ必要?」感の有る仕様が有ったりしますね。

servletベースのWAFはどうやって回避しているのでしょうか?

Scalatraでずっとissueが残っていますが、特にservletがバージョンアップしても回避策は無さそうなので、APIに注意喚起するPRだけ作っておきました。

github.com

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