HackTestを書いてみよう
今回は前回紹介したHackTestを使って、Hackの実装コードに対して
ユニットテストを記述してみましょう。
まずは手元にVisual Studio Code + Hack Pluginなどをご用意ください。
プロジェクトディレクトリ
用意するファイルとディレクトリは下記の構成になります。
事前に src
ディレクトリと tests
ディレクトリを作成します。
├── .hhconfig
├── composer.json
├── hh_autoload.json
├── src
└── tests
.hhconfig
ここでは下記のものを利用します。
PHPのコードは書きませんので、PHPコード混入なしとし、
全てstrictで記述します。
assume_php = false
ignored_paths = [ "vendor/.+/tests/.+" ]
safe_array = true
safe_vector_array = true
composer.json
composer.jsonは手動で作ってもコマンドで作ってもどちらでもいいです。
$ hhvm $(which composer) init
hh_autoload.json
composer.jsonが用意できたら、hh_autoload.jsonをあらかじめ作っておくと便利です。
(あとで作成してもOK)
hh_autoload.jsonの内容は次の通りです。
{
"roots": [
"src/"
],
"devRoots": [
"tests/"
]
}
作成したディレクトリを記述します。
rootsは実装コードを配置するディレクトリ、
devRootsはテストコードなど開発時にしか使わないコードを配置するディレクトリです。
hh-autoload.jsonでディレクトリを指定しますので、
composer.jsonでpsr-4の指定はしなくて構いません。
hhvm/hhvm-autoloadが、
指定されたディレクトリをスキャンして、クラスファイルやxhpのファイルやら、
取得してautoloaderを作成するため、指定してもあまり意味はありません。
(ディレクトリと名前空間を認知させるのには役立ちます)
HackTest、fbexpectのインストール
下記のコマンドでまとめてインストールしましょう。
$ hhvm $(which composer) require --dev hhvm/hacktest facebook/fbexpect
導入時期などでバージョンはことなりますが、
以下の様にHackのライブラリがインストールされれば準備完了です。
Package operations: 8 installs, 0 updates, 0 removals
- Installing hhvm/hhvm-autoload (v1.7): Loading from cache
- Installing hhvm/hsl (v3.29.1): Loading from cache
- Installing hhvm/type-assert (v3.2.6): Loading from cache
- Installing facebook/difflib (v1.0): Loading from cache
- Installing hhvm/hsl-experimental (v3.29.5): Loading from cache
- Installing facebook/hh-clilib (v2.0.1): Loading from cache
- Installing hhvm/hacktest (v1.3): Loading from cache
- Installing facebook/fbexpect (v2.3.0): Loading from cache
当然ですがPHPでおなじみのライブラリはなく、Hackのライブラリのみです。
composer.jsonを手動で用意する方は下記のものを使えばOKです。
{
"name": "acme/testing",
"type": "project",
"minimum-stability": "stable",
"require": {
"hhvm": "^3.29.0"
},
"require-dev": {
"hhvm/hacktest": "^1.3",
"facebook/fbexpect": "^2.3"
}
}
尚、2018/12/15現在最新の3.29以下のバージョンは本記事ではサポートしません。
とりあえずHackTestを動かす
コマンドは、簡単!
./vendor/bin/hacktest [テスト対象のディレクトリ]
これだけですので、とりあえずきちんと動くかどうか確認しましょう。
下記の通りに実行して同じ結果になれば問題ありません。
$ hhvm ./vendor/bin/hacktest tests/
Summary: 0 test(s), 0 passed, 0 failed, 0 skipped, 0 error(s).
はじめてのHackTest
それでは早速テストを書いてみましょう。
まず tests配下に ConvertTest.phpファイルを作成します。(.hhファイルでも構いません。)
ここに以下の内容のテストを記述し、HackTestを動かしてみましょう。
<?hh // strict
use function Facebook\FBExpect\expect;
use type Facebook\HackTest\HackTest;
final class ConverterTest extends HackTest {
public function testHello(): void {
expect('Hello')->toBeSame('Hell' . 'o');
}
}
これまでに紹介した様に、Hackで厳格モードで実装する場合は、<?hh
の後に // strict
を記述します。
PHPから分離するHHVM4.0に向けて、
これからHackを使う場合は基本的にstrictで実装することをオススメします。
PHPで見慣れない記法はおそらくuseの微妙な違いでしょうか?
Hackではクラスをインポートする場合は、 use type Acme\Component\ClassName
となり、
名前空間をインポートする場合は、 use namespace Acme\Component
です。
use function
はPHPと同じですね。
これで先ほどのコマンドでHackTestを実行します。
以下の結果になればOKです。
$ hhvm ./vendor/bin/hacktest -v tests/
.
Summary: 1 test(s), 1 passed, 0 failed, 0 skipped, 0 error(s).
実装しながらのHackTest
ここからはTDDライクに進めていきましょう。
先ほどのテストに加えて次のテストを追加します。
<?hh // strict
use function Facebook\FBExpect\expect;
use type Facebook\HackTest\HackTest;
final class ConverterTest extends HackTest {
// 省略
public function testSimpleConversion(): void {
$input = '{"key":"value","key2":"value2"}';
$output = [
'key' => 'value',
'key2' => 'value2'
];
$converter = new \Acme\Converter\Converter();
expect($output)->toBeSame($converter->convertString($input));
}
}
テストクラスを追加したらhh-autoloadに登録させる必要がありますので下記のコマンドを実行します。
$ hhvm ./vendor/bin/hh-autoload
上記のコマンドでhh-autoloadに登録後、テストを実行します。
当然失敗します。PHPと同じですね。
$ hhvm ./vendor/bin/hacktest -v tests/
.E
1) ConverterTest::testSimpleConversion
Class undefined: Acme\Converter\Converter
ただし、Hackの場合はhh_clientを実行してクラスそのものが存在するかどうかチェックできますので、
実際のテストを伴う開発でこのフローで進むことはまずありません。。。
PHPのPHPUnitのチュートリアル等と合わせた内容にしています
みなさんの環境・開発チームで取り入れる時は、まず最初に hh_client
を実行しましょう。
この状態でhh_clientを実行すると下記の通りエラーとなります。
$ hh_client
tests/ConverterTest.php:18:22,46: Unbound name: Acme\Converter\Converter (an object type) (Naming[2049])
テスト対象のクラスが存在しなかったため、実際のクラスを用意しましょう。
src/Converter/Converter.php ファイルを作成し、下記のクラスを記述。
その後に $ hhvm ./vendor/bin/hh-autoload
を実行します。
<?hh // strict
namespace Acme\Converter;
class Converter {
}
当然失敗しますが、メソッドがない、というエラーに変わったことを確認しましょう。
$ hhvm ./vendor/bin/hacktest -v tests/
.E
1) ConverterTest::testSimpleConversion
Call to undefined method Acme\Converter\Converter::convertString()
ここまで正常に進んでいれば、あとは実装をどんどん進めていくだけです。
下記のメソッドを Acme\Converter\Converter
クラスに追加しましょう。
public function convertString(string $input): ?array<mixed> {
}
この状態でテストを実行すると失敗になります。
動かして確認してみましょう。
$ hhvm ./vendor/bin/hacktest -v tests/
.F
1) ConverterTest::testSimpleConversion
Failed asserting that array (
'key' => 'value',
'key2' => 'value2',
) is the same as NULL
テストを見れば分かる通り、これはjsonを扱う処理の様ですので、
愚直にjson_decodeを使う様に実装します。
<?hh // strict
namespace Acme\Converter;
use function json_decode;
class Converter {
public function convertString(string $input): ?array<mixed> {
return json_decode($input);
}
}
感のいい方はわかるかもしれませんが
json_decodeは第二引数のassocで連想配列かstdClassで返却するかを選ぶことができます。
Hackは肩に強いはずなので、戻りの型がTypechckerですぐわかるんじゃ・・?
実はPHPから移植された関数の多くは、PHP同様に戻りの型が不明確なままです。
つまりmixedで返却されるため、この時点でTypecheckerで引っ掛けることができないため、
実装中に気付かないケースとなります。
したがってこのテストを実行すると、PHP同様にテスト結果は次の通りになります。
$ hhvm ./vendor/bin/hacktest -v tests/
.E
1) ConverterTest::testSimpleConversion
Value returned from method Acme\Converter\Converter::convertString() must be of type ?array, stdClass given
arrayで返却する様に宣言されてるけど、stdClassで返ってるよ、という結果になります。
PHP同様に第二引数をtrueとして、連想配列を返却する様にしましょう。
return json_decode($input, true);
変更後HackTestを実行すると成功します。
$ hhvm ./vendor/bin/hacktest -v tests/
..
Summary: 2 test(s), 2 passed, 0 failed, 0 skipped, 0 error(s).
Hackのjson_decodeについて
Hackにしか存在しないオプションがいくつかありますので、実際に利用するものを紹介します。
JSON_FB_COLLECTIONS
Hackのコレクションで返却するオプションです。
json_decode($input, true, 512, \JSON_FB_COLLECTIONS),
これを利用すると次の様に返却されます。
object(HH\Map)#85 (2) {
["key"]=>
string(5) "value"
["key2"]=>
string(6) "value2"
}
JSON_FB_HACK_ARRAYS
Hackの配列で返却するオプションです。
json_decode($input, true, 512, \JSON_FB_HACK_ARRAYS),
dictで返却される様になります。
dict(2) {
["key"]=>
string(5) "value"
["key2"]=>
string(6) "value2"
}
はじめてのDataProvider
いくつかのjson変換のテストをしたい場合などでDataProviderが利用できます。
DataProviderとするメソッドをテストクラスに追加します。
入出力のペアのDataProviderを作ることが多いと思いますが、
この場合はtupleを組み合わせて作成するのが簡単です。
public function conversionSuccessfulProvider(): vec<(string, array<mixed, mixed>)> {
return vec[
tuple(
'{"key":"value","key2":"value2"}',
[
'key' => 'value',
'key2' => 'value2',
],
),
tuple(
'{"key":"value","key2":"value2","some-array":[1,2,3,4,5]}',
[
'key' => 'value',
'key2' => 'value2',
'some-array' => [1, 2, 3, 4, 5],
],
)
];
}
vecとtupleの組み合わせ、覚えておきましょう。
テストクラスに記述したtestSimpleConversionメソッドで、
このDataProviderを利用するように変更します。
DataProvider Attributeを使って指定します。
<<DataProvider('conversionSuccessfulProvider')>>
public function testSimpleConversion(
string $input,
array<mixed, mixed> $output
): void {
$converter = new \Acme\Converter\Converter();
expect($output)->toBeSame($converter->convertString($input));
}
public function conversionSuccessfulProvider(): vec<(string, array<mixed, mixed>)> {
return vec[
tuple(
'{"key":"value","key2":"value2"}',
[
'key' => 'value',
'key2' => 'value2',
],
),
tuple(
'{"key":"value","key2":"value2","some-array":[1,2,3,4,5]}',
[
'key' => 'value',
'key2' => 'value2',
'some-array' => [1, 2, 3, 4, 5],
],
)
];
}
これでHackTestを実行します。
$ hhvm ./vendor/bin/hacktest -v tests/
...
Summary: 3 test(s), 3 passed, 0 failed, 0 skipped, 0 error(s).
テストがケースが増えて、成功していることを確認しましょう。
DataProviderとbeforeEachTestAsync
PHPUnitでよく利用するsetUpメソッドの代替は、beforeEachTestAsyncメソッドを利用します。
メソッド名にAsyncとサフィックスがある通り、
利用時は下記の記述になります。
public async function beforeEachTestAsync(): Awaitable<void> {
echo 1;
}
このbeforeEachTestAsyncメソッドですが、
PHPUnitのsetUpメソッドと同じで、DataProviderに記述した値をテストで利用するときに必ず実行されますので、
同様に利用することができます。
ぜひテストに活用してみましょう。
おわりに
HackTestの簡単な利用方法についてチュートリアル形式で紹介しました。
基本はこれだけになりますので、PHPUnitと同様に利用できますので、
これからHackを始める方は是非テストをたくさん書いてください!
テストの内容については実はまだまだHackならではの方法で様々な型を使うことができます。
それはまた別の機械にしましょう。