chobi_e

hhvmのExtension書いてみた

みなさんこんにちは。hackしてますか?

今日はhhvmのC++拡張(Extension)について書いてみます。

前振り

hhvmはfacebookが開発・公開しているPHPの処理系のうちの一つでC++で書かれており、linux上でのJITがサポートされており場合によってはとても高速にPHPアプリケーションを実行する事ができます。

勿論Native拡張を書くこともでき、既存のライブラリ資産の有効活用やどうしても速度が出ない部分の改善などが簡単に行えるので手段として知っていると便利です。

この記事をきっかけにhhvm Extensionのとっかかりになれば嬉しいです。

続きを読む
KuokaYusuke

Mavenで結合テストを自動化する方法

こんにちは、九岡です。

Javaエンジニアのみなさん、結合テストの自動化してますか?!

この記事では、

  • 結合テストとは何か
  • 筆者は何のために行っているのか
  • それをMavenで自動化する方法

をご紹介します。

用途が知られていたりいなかったり、単体テストに比べると情報が少なかったり、より多くのMavenプラグインを使うことになりがちで手間がかかる「結合テストの自動化」。

「まだやってない」という方は、この記事をとっかかりにしていただけるとうれしいです!

続きを読む
gong023

レガシーなプロダクトにテストで向き合う話

はじめまして。荻原といいます。グリーのプラットフォーム部門で、サーバーサイドのエンジニアをしています。

昨年末ぐらいまで業務の空き時間にテスト周りでごにょごにょと動いていたので、今日はそのことについて書かせて頂きます。

こんな人は読むと役に立つかもしれません。

  • レガシーなプロダクトになんとかして突破口を開きたい
  • PHPUnit の書き方で参考になりそうなものを探している
  • Ruby でスマートフォンのブラウザ操作を自動化したい

経緯

こちらでも言及されている通り、サービスを運営している以上、時には技術的負債に向き合わなければなりません。GREEも歴史が長いプロダクトなので、日々コードをリリースしていく中でそういった問題に頭を抱える場面もありました。

技術的負債による副作用はたくさんありますが、どういう点に不安を感じていたのか、実際に開発の現場に立って感じたことをいくつか書いてみたいと思います。

コードが読めない・・・

私がチームにアサインされた当初、担当範囲のコードを読み解くのに非常に苦労しました。もちろんこれは私のリーディング力が低かったからというのもあると思いますが、それを差し引いてもコード以外に理解しなければならない業務知識も多く、チームにジョインしていきなり仕事をするのはほとんど不可能なように思われました。

しかし、そんな状況でもタスクは存在します。業務に慣れないうちは、中途半端な理解でだましだましコードを書くしかありませんでした。お恥ずかしい話ですが、しばらくはそんな状態で仕事をしていて、先輩に「あーそれとそれの順番かえちゃダメだから。」という類の指摘を受けたこともありました。この時言われてなければ大障害を起こしていたと思うと今でもぞっとします。

それから時間が経ち、ある程度業務知識も増え、コードの全体像も理解できるようになったころ、かつて作った部分をもう一度メンテする機会がありました。以前やった箇所だから簡単だろうと思ってファイルを開くと、コードには無邪気に if や for が積み上げられていて愕然としました。今だったら業務背景も理解しているしコードの全体像もわかる。このディレクトリのこんなクラスにこんなコード書く必要ないのにと思いつつ、あんなに苦労させられたコードの加害者の一員に、知らぬ間に加わっていたのだということに気づいてショックを受けました。

プロダクトがこういう状態になってしまうと

  1. コードが読めない
  2. よくわからないから適当に注ぎ足す(故意・過失に関わらず)
  3. さらにコード読めなくする

というサイクルに入ってしまい、もはや割れ窓理論状態です。

リリース前テストが終わらない・・・

私のチームでは、コードが完成し、レビューも完了となると、次はステージング環境での受け入れテストというフローになっていました。

一つ補足しておくと、私が今回言及する「受け入れテスト」とは「ユーザーから見て、対象のソフトウェアが受け入れられるものかどうかテストする」という意味です。もっと根も葉もない言い方をすると、ポチポチ端末をいじってテストすることです(少なくとも今回の記事ではそういう意味で使っています)。近い意味としてよく用いられる言葉は、ブラックボックステスト、統合テストなどでしょうか。

受け入れテストは、デベロッパー目線ではなくユーザー目線であることに注意して下さい。私のチームではこれを徹底するため、受け入れテストは専門のチームに依頼していました。開発者自身がテストする場合、修正箇所だけを確かめがちになりますが、そうではなく、第三者にユーザー目線で広くテストしてもらうことは品質担保という意味で非常に妥当だと言えます。

しかしそれゆえに、時間もかかります。開発者からすれば修正箇所以外のテストも行われるので無駄なような気がしてしまうのですが、実際に予期していない箇所から不具合が検出されることもありました。思わぬところで影響が出てしまうというのは技術的負債を背負ってしまったプロダクトならではという感じですが、だからこそしっかり確認していかなければなりません。

また、時間がかかる要因として、非常に多くのテストケースをカバーしなければならなかったことも挙げられます。基本的に歴史の長いプロダクトであるほど満たさなければならない仕様が多く、色々な条件を掛けあわせていくと場合分けがどんどん発散していきます。さらにそれを様々な端末・OSでテストするので、テストケースは膨大になってしまいます。テストケースが膨大になると、今度はテストを依頼するチームとのコミュニケーションコストも無視できない問題になってきます。

しかしいくら時間がかかるとはいえ、これは決してスキップすべきフローではないように思いました。ミッションクリティカルな部分を触ることも多いチームだったので、些細なものでも不具合を看過すべきではありませんでした。

そんなこんなで、私はコードレビューが完了してから全ての修正を出すまでに二ヶ月あいたということも経験したことがあります(非常に極端な例でしたが)。確かにテストはスキップすべきではありませんが、同じような端末操作が行われていることも多かったので、自動化できればリリースを効率化できるのではと考えていました。

どうしよう

勢いで色々と書きましたが、一旦感じていたことをまとめてみようと思います。

  • コードの汚染度が高く、必要とされる業務ドメインをコードから読み解くのに苦労するし、その状況がまた汚染を加速させる
  • 端末を操作してテストする作業がリリースのボトルネックになっており、自動化したい

大胆にリファクタリングを行っていくという選択肢もあるのかもしれませんが、多くの方々に利用して頂いているプロダクトに大幅な修正をしていくのは大変です。また、リファクタリングを加えるにせよ、それの前と後で挙動に差がでていないか確認する手段が必要です。そういった経緯で、テスト周りの強化に目が向きました。考えていたのは以下の二点です。

  • 単体テストを強化し、ドキュメントとする
  • 端末操作を自動化し、業務を効率化する

単体テストを強化するモチベーションは様々ですが、私の場合は特にドキュメントとしての効果を期待しました。テストを強化する前からドキュメントはあったのですが、どうしても wiki のような形式だとリリースするそばから古くなっていってしまいます。日々書き換わるコードに対して正しさを保証するためには、単体テストをドキュメントとして作っていくのが良いと思いました。イメージとしてはコードの翻訳という形に近いかもしれません。

受け入れテストに関しては、同じような端末操作を自動化することで直接的な効果を期待出来ました。ただし、専門のチームとコミュニケーションを取るためにも、非エンジニアの方でもどのようなテストをしているのかわかりやすいようにすることと、より広い範囲をカバーできるよう多端末でテストできるような仕組みにすることを念頭に置きました。

ドキュメントとしての単体テスト

単体テストにはドキュメントとしての効果を期待していましたので、可読性には特に気を払っていました。ここでは実際にサンプルを挙げて、どのような書き方をしていたかを紹介したいと思います。なお、テスティングフレームワークは PHPUnit を想定しています。

今回は以下のようなコードをテストターゲットととして考えてみます。

よく見かけるのは以下のようなテストコードです。

残念ですがこれは、可読性うんぬん以前に異常系をカバーできていません。意外に書かれていない場合が多いです。とりあえず異常系を書き足してみましょう。

というわけで書き足しましたが、同じ関数の中に複数のテストケースが混在しているのは頂けません。テストコードでは、関数を振る舞いごとに分けましょう。余談ですがこういうことを繰り返して、たまにテストコードなのに100行や200行もあるモンスター級の function になっているのを見かけたりします。こうなってしまうと何をテストしたいのかわからなくなってしまいます。もっと酷い場合 if や foreach などのロジックがふんだんに入ってテストが間違っているのか元のコードが間違っているのかの区別もつけられない時もあります。

異常系、正常系を分けました。私の場合、正常系・異常系ごとにテストケースを @group アノテーションで振り分ける場合が多いです。こうすることでテストの意図を明確にできます。

良くなってきましたが、もう少し読みやすく書けます。振る舞いで関数を分けたのなら、関数名もそれに合わせてしまいましょう。ついでに @test アノテーションをつけて、関数の頭の test という prefix を削ってしまったほうが読みやすいでしょう。

いかがでしょうか。全て同じコードに対するテストですが、書き方の工夫次第でかなり可読性が違うと思います。サンプルは isOverTen という prefix ではじめていますが、実際はテスト対象の関数ごとにテストクラスを一つ作っていたので、この部分は省略していました。テスト対象のクラスがかなりファットになってしまっている場合、テストクラスの方を細かくしないと対応していけません。

fixture の扱いも気をつけたいところです。何も考えずに一つの array で管理していたりすると、どのテストデータがどういう意図で設けられているのか全く読めないです(というか既存のテストがそうなっていて苦労しました)。今は PHP にも fixture をうまく管理するライブラリが作られているので、積極的に導入していって良いと思います。そもそもDBアクセスさせないという方針もアリで、DBデータは極力 mock を使うようにしていました。私はテストが失敗した時にDBの状態とロジック両方を疑わなければならない状態はあまり好きではないです。

また、弊社はフレームワークに Ethna を使っていますが、標準のテストクラスが SimpleTest の利用を前提としていたため、こちらでヘルパーを作りました。ついでにヘルパーにはスーパグローバル変数を操作して iPhone や Android の UserAgent をエミュレーションする機能を乗っけたりしました。

Ruby 周辺のツールで受け入れテストを自動化する

冒頭で述べたように、受け入れテストも効率よく行っていかないとリリースのボトルネックになってしまいます。今回は RSpec + turnip + selenium-webdriver + Appium という構成で自動化を実現しています。これに関しては実際にコードをみて確かめてもらったほうが早いので、今回は「ブラウザを立ち上げgoogleにアクセス > 検索ボックスにワードを入れて submit > 検索結果をスクリーンショットで保存」というブラウザ操作を自動化したサンプルを用意しました。是非一度動かしてみてください。

サンプルの動作方法は以下です。なお、サンプルは Ruby2.0 以降で動くので、事前にインストールしておいて下さい。

いかがでしょうか。うまくいっていれば Firefox が立ち上がりブラウザ操作が走ったかと思います。またインストールしたディレクトリの pic 以下に、検索結果のスクリーンショットが保存されているので確認してみてください。

せっかくなので、iOS でも動かしてみましょう。実機でも動かすことはできますが、サンプルではお手軽に iPhone Simulator で動くようになっています。Ruby に加え、事前に以下の環境を整えておいて下さい。

  • node.js v0.10以降
    • Xcode 4.6.3以降
    • command line tools
  • Appium 0.14.2以降
    • 手元では、npm から取れる CLI ではなくこちらの GUI 版を使っています

環境が整ったら、Appium を launch して、以下のコマンドを実行してみてください。うまくいけば iPhone Simulator が立ち上がって先ほどと同じ操作が行われると思います。スクリーンショットが pic 以下に保存されるのも Firefox の時と同様です。

さて、iOS で動かせたら、次は Android です。Android は実機で動かすハードルが低いので、サンプルでも実機をターゲットにしてあります。環境面では、Appium を利用していないので node.js や Xcode は不要です。代わりに、以下のものをセットアップしておいて下さい。

  • Android SDK
    • adb
  • android-webserver
  • adb install android-server-2.21.0.apk で対象の端末に apk をインストールしておいて下さい。

セットアップが終わったら、以下のコマンドで試せます。

以上のサンプルをベースとして、チームではよくテストするブラウザ操作を jenkins の「ビルド実行」をクリックすれば行えるようにしてあります。リグレッションテストで役立つのはもちろんですが、定期的に動かして監視ツールとしても使えます。(ただ、現実は端末が wifi をつかめずテスト失敗したりするので、監視ツールとしての運用は難しい部分も多いですが・・・。)

さて、ここまで試して頂けたらコードもあることですし説明することは実はあまりないのですが、最後にいくつかこの構成を選択した理由を挙げておきます。

1.feature ファイルが魅力的

以下はサンプルの spec/acceptance/firefox/google_search.feature の抜粋です。

どんなテストをしているのかひと目でわかると思います。そのため「このテストをこうして欲しい」といったコミュニケーションも簡単にとることができます。Gherkin をしっかりサポートできていそうなのは Ruby,Java,JavaScript,.NET でしたが、個人的な好みもあって RSpec + turnip を使っています。

ついでなので selenium-webdriver を使っている理由も簡単に説明しておきます。最初は driver に capybara を使っていたのですが、capybara が単に selenium-webdriver をラップしていたので、直接触ってしまって良いだろうということで selenium-webdriver を採用しました。特に capybara に不満があったというわけでもないのですが、こういうスクレイピングのような作業は繊細なので、うまく動かなかった場合に疑う対象は一つでも少ないほうが良いと考えました。

2.iOS, Android のブラウザをターゲットにできる

昨今はメインターゲットがスマートフォンだという場合が多いと思います。GREEもその例に漏れずスマートフォンからの利用が多いプロダクトなので、iOS 及び Android のブラウザからテストが行えるという点はかなり重要視していました。そういった意味では calabash でも良かったのですが、native アプリのサポートしかなく、iOS Safari ではテストできなさそうだということで使用をやめました。代わりに Appium を採用し、iOS でのテスト環境は当初想定したとおりに作ることができました。そのまま Appium で Android の環境も構築しようと考えましたが、chromedriver がデバイスの root を取らないと使えないということで見送り、代わりに端末に android-webserver を立てる形で対応しました。

雑感

少し脱線してしまいますが、色々やらせて頂いて学ぶことも多かったので雑感を書き連ねてみようと思います。

効果ある?

パピルスから紙に変えた効果を定量的に説明するのは難しいです。そして、テストを充実させるというのはそういう類の話だと思います。

また、開発に銀の弾丸はないとはよく言われますが、それ以前の問題としてテストは充実させるのは必要条件だと思います。

テスト書くと工数倍になる?

確かに書かなければならない行数は増えますが、TDD,BDD で開発する場合仕様やバグの確認を行いながら開発できるので、手戻りがなくかえって開発を早く行えるケースも珍しくないと思います。また、自身の書きたいコードを明文化しながら開発できるという点も大きなメリットで、これも開発効率を向上させる追い風になります。

逆に後追いでテストを書いていく場合、開発時の不安を思い出せないので大変です。

受け入れテストって難しい?

受け入れテストに使えるツールは本当に色々開発されているので、選ぶのが大変でした。またツールに一癖ある場合が多く、少し慣れが必要です。例えば対象ページのHTMLがうまく取得できず悩んだが、実際はページのレンダリングが終わらない内に要素を取得しにいっていたので、再試行する必要があった、などといった話です。こういう場合特にエラーを発したりしないのでこちらで原因にあたりをつけなければなりません。

方法論に関していえば、いわゆる普通のソフトウェアエンジニアの中でメジャーになっているものはあまりない印象ですが、かなり研究されており興味深かったです。一応調べた中ではハードウェアメーカーのテスト技法が面白かったです。HAYST法、CFD法、状態遷移図などを使えばより効率的にテストケースを洗い出せたかもしれません。またISTQB,JSTQB といったものもあるので、学んでみると面白そうです。

まとめ

抱えていた問題と対処策

  1. コードが読めない
    • 単体テストをドキュメントにする

      • (テスト対象関数名)_ Return(返り値) _When(条件) にした
  2. 受け入れテストに時間がかかる
    • 端末操作を自動化する

      • RSpec + turnip + selenium-webdriver + Appium を使った

なんだかまとまりのない記事になってしまいましたが、ひとつの事例として皆様の参考になれば幸いです。

よや

SWFバイナリ編集のススメ番外編 (zlib 伸張) 中編

よやと申します。こんにちわ。

今回は zlib 解説の中編です。前編はこちら ↓

前回、zlib ヘッダのバイナリを解説しました。今回はその後ろに続く Deflateストリームのバイナリ構造についてです。

尚、固定ハフマンまでの説明で長くなり過ぎたので、動的ハフマン(カスタムハフマン)は次回の後編で改めて解説いたします。

前回の復習

  • zlib は「zlib ヘッダ + Deflate ストリーム + ADLER32」のデータ形式です。尚、通常、zlib ヘッダ無しでも Deflate ストリームだけで圧縮元のデータを復元出来ます。(DICTID が存在すると話が少し厄介ですが、通常見かける事はないでしょう)

zlibInflate-zlib

Deflate はデータを任意の長さのブロックで分けて収納出来ます。続きのブロックがあるかは BFINAL で表し、各ブロック毎に任意の圧縮タイプ(BTYPE)を付けられます。

  • ブロックの連結イメージ

zlibInflate-Deflate

  • 圧縮タイプには三種類存在します。データのサイズや性質に応じて使い分けます。(前回、大まかに解説しました)

zlibInflate-Deflate-BTYPE0zlibInflate-Deflate-BTYPE1zlibInflate-Deflate-BTYPE2

テストデータの作り方

PHP には gzcompress という関数がありまして、gz と頭に付いていますが (gzip でなく) zlib のデータを生成します。

  • 無圧縮(BTYPE:0)

  • 固定ハフマン(BTYPE:1)

  • 動的ハフマン(BTYPE:2)

小さなデータに対して動的ハフマンを適用する方法は http://d.hatena.ne.jp/n7shi/20110719 を参考に .NET の DeflateStream を使おうとしたのですが、環境を作るのが面倒なのでデータの方だけ拝借させて頂きます。(zlib ヘッダとして頭に 9c 78 の 2byte を追加)

BTYPE 別詳細解説

BTYPE:0 無圧縮

この方式は実際には圧縮しません。初めにデータ長があり、その後ろに何も変換しない生のデータが続きます。

但し、データ長フィールドは 2 bytes の為、65535 を超えられない事に注意が必要です。

zlibInflate-Deflate-BTYPE0

先程、生成したテストデータ(btype0.zlib)で確認しましょう。

先頭 2 bytes は zlib ヘッダなので無視するとして、その後ろに BTYPE とデータ長が続きます。

  • zlib のビットの読み出しは分かりにくいですが、下位ビットから読みだします。
  • 0×01 は2進数で 00000001、最後1ビットの 1(最後のブロック)とその手前2ビットの 00 (BTYPE)だけ意味があります。頭の 00000 は読み捨てます。

DeflateBTYPE0_70per

  • LittleEndian なので 0c 00 => 0x000c => データ長が 12 byte である事を示します。
  • データ長の次には足すと 0xffff になるような値(*2)が入ります。(verify チェックでしょうか)

12 bytes データが続く事が分かっているので、”This is TEST” を取り出して、復元成功です。

最後の 4 bytes は ADLER32 のチェックサムです。データが化けてないか確認するのに使います。

BTYPE:1 固定ハフマン

とある決まったハフマン表を使って伸長します。

zlibInflate-Deflate-BTYPE1

BTYPE=1(固定ハフマン)のハフマン表は RFC1951 の Page12 に載っています。

これをビット数の少ない順に並べて図にするとこうです。

固定ハフマン変換表_70per

  • 終端や繰り返しを表すメタデータに短いビット(7bits)を割り当て、
  • その次に短いビット(8bits)を ASCII の前方のコードや距離の長い繰り返しのメタデータに割り当て、
  • 最後にASCIIの後ろの方の文字を割り当てる。

US-ASCII のテキスト文字で短い間隔での繰り返しが多く出る、普通の英語の文章にそこそこ合った(でも、文字頻度での最適化まではしていない)ハフマン表と言えます。

さて、この表を元に先程作成した、テストデータ(btype1.zlib)を解釈してみます。

  • 0x0b は2進数で 00001011、最後1ビットの 1(最後のブロック)とその手前2ビットの 01 (BTYPE)で始まります。
  • btype:0 と違って、残りの 00001 にも意味があります。そこからデータの始まりです。

まずは BFINAL と BTYPE の 3bit を見て固定ハフマンのブロックが始まるのを確認します。

DeflateBTYPE1_LIT_1_70per

元データの復元 (リテラル)

まず 7 bits 読みだします。

DeflateBTYPE1_LIT_2_70per

7bits のエントリで定義された符号の範囲に収まらないので、更にもう 1 bit 読みだします。

DeflateBTYPE1_LIT_3_70per

これで、1文字目の “T” が取り出せた事になります

元データの復元 (繰り返し)

“This is TEST” の “is “の3文字が繰り返しなので、長さ3、距離3の繰り返しを表すメタデータをハミング符号で指定する事で、圧縮率を上げられます。

DeflateBTYPE1_LEN_1_70per

繰り返しメタデータに関する符号は RFC1951 の Page12 に定義されています。

  • ハフマン符号に応じて、繰り返しの長さが定義されます。
  • 繰り返しの長さが長い(11以上の)場合は、その長さに応じて追加の長さを入れるフィールド(Extra Bits)を用意します。
  • 続く5bits に繰り返し開始までの距離が入ります。
  • 繰り返しの距離が遠い(11以上の)場合は、その距離に応じて追加の距離を入れるフィールド(Extra Bits)を用意します。

バイナリ構成としては以下のようになります。

  • Huffman Code (Length)

DeflateBTYPE1_LEN_2_70per

  • Distance Code

DeflateBTYPE1_LEN_3_70per

では、実際のデータ(“is ” の繰り返し)を見てみましょう。

繰り返しのある場所まで調べるのが面倒なので、ツールに頼ります。

これは pure PHP で Zlib を解釈するツールですが、これの dump スクリプトを使うと、各ハフマン符号が Zlib バイナリ中の何処のオフセットに存在するのか分かります。

これを信じて (0オリジンの) 7 bytes 目の 3bits 目から分解します。

  • ハフマン符号が 0×257 なので繰り返しの長さが 3 だと分かります。

DeflateBTYPE1_LEN_4_70per

  • 続く 5 bits を見ると、距離が 3 だと分かります。

DeflateBTYPE1_LEN_5_70per

この例では、Extra Bits を使いませんでしたが、より長い長さ、遠い距離だと必要になり、上の方で説明したように Extra Bits に入っている値を足して補正します。

BTYPE1 のまとめ

  • ハフマン符号を解釈して以下の3つに分別します。

    • リテラル(0~255) => 元データそのもの
    • 終端記号(256) => Deflate ブロックの終端
    • 長さ(257~285) => 繰り返しパターンの長さ
  • ハフマン符号が長さを表す場合

    • 長さが 3~10(ハフマン符号は 257~264)の場合は、その値をそのまま長さとして使う
    • 長さが 11以上(ハフマン符号は 265~285)の場合は、更に後ろを数bits読みだして、それを足した値を長さにします
  • 距離フィールド(5bits)

    • 距離が 0~3 の場合は、その値をそのまま距離として使う
    • 距離が 4以上の場合は、更に後ろを数bits読みだして、それを足した値を距離にします
  • ビットをそのままの並びで値として解釈する時と、逆さにして解釈する時があります。規則は正直分かりません。ハフマン符号が逆順なのは妥当ですが、5 bits 固定の長さフィールドも逆順で、その拡張ビットは正順。整理すると以下のようになります。

    • ビット正順 => BTYPE の 2 bits、長さ拡張フィールド、距離拡張フィールド。
    • ビット逆順 => ハフマン符号、距離フィールド(5 bits)

次回予告

BTYPE:2 動的ハフマン

より高いデータの圧縮率を求めるならば、そのデータに応じた適切なハフマン表が必要です。

この動的ハフマン方式では、元データを解析して適切なハフマン表で圧縮を行い、そのハフマン表を先頭につけるデータ構造となります。

こちらは最終回に当たる Zlib 伸長解説の後編で解説いたします。

以上です。

*1: gzip ヘッダも込みで欲しい場合は gzencode 、zlib のヘッダ無しで deflate ストリームだけ欲しい場合は gzdeflate が対応しています

*2: いわゆる 1の補数です。一般に負の表現でよく使われる 2の補数ではないです。