Hackでユニットテスト HackTest 入門


Hackでテストっでどうするの?

今回はそんなHackのユニットテストについてです。

これまでのHackのテスト

これまでHackのユニットテストでは、
PHPと同様にPHPUnitを使うか、HackPack/HackUnit を使うという選択肢がありました。
後者は2年ほど前から開発が停止しており、
現在のHHVMのバージョンにも追従していないため、
利用はオススメしません。(多分動きません)

ということで選択肢は実質PHPUnitのみ、という状態でした。
ただHackはユニットテストは基本的にstrictで記述するため、
PHPUnit対応のhhiを同時にインストールしなければなりませんでした。

composer.json
{
  "require": {
    "hhvm": ">=3.27.0",
    "91carriage/phpunit-hhi": ">=5.7.3"
  },
  "require-dev": {
    "phpunit/phpunit": ">=5.7"
  },
  "autoload": {
    "psr-4": {
      "Acme\\Hack\\": "src/"
    }
  }
}

これ以外はPHPとテストの書き方は変わりませんでした。

これからのHackのテスト

これからのHackですが、下記で発表されているように
基本的にPHPのサポートは終了予定です。
Ending PHP Support, and The Future Of Hack
このため、HHVMでPHPを実行するメリットもなければ、
HackでPHPのライブラリを使い続けるメリットもありません。

  • 2018-12-03: branch cut: expect PHP code to stop working with master and nightly
  • builds after this date
  • 2018-12-17: expected release date for v3.30.0
  • 2019-01-28: expected release date for v4.0.0, without PHP support
  • 2019-11-19: expected end of support for v3.30

Hackのライブラリ開発者もPHPのライブラリをHackへ移植していたりするところで、
Hackで開発している開発者は現在 hhvm/hacktest へ移行しています。
これによってこれまでPHPUnitに依存していたものがHackのみとなっており、
PHPライブラリ依存離れが急ピッチで進んでいる訳です。
実際のアサートなどは hhvm/fbexpect を利用です。

今回はHackTestについて紹介します。

HackTest

インストール方法はcomposerですので、PHPと変わりません。

$ hhvm $(which composer) require --dev hhvm/hacktest facebook/fbexpect

composer自体はかならずhhvmで動かしてください。
PHPの場合はインストールができません。
準備はこれだけです。

PHPUnitからの移行

PHPUnitから移行したい場合は、hhvm/hhastを利用します。
対象のテストコードがあればHackUnitに対応してくれるというものです。

例えばこんなコードの場合。。

<?hh // strict

use type PHPUnit\Framework\TestCase;

final class HackExampleTest extends TestCase {

  private Map<int, string> $m = Map{};

  protected function setUp(): void {
    $this->m->addAll([
      Pair{1, '1'},
      Pair{2, '2'},
      Pair{3, '3'},
    ]);
  }

  public function testShouldBeSameMap(): void {
    $this->assertSame(new Map([
      1 => '1',
      2 => '2',
      3 => '3',
    ]), $this->m);
  }
}

migrateを実行します。

$ git clone https://github.com/hhvm/hhast.git
$ cd hhast
$ hhvm ./bin/hhast-migrate --phpunit-to-hacktest /path/to/myproject/tests

migrate実行後は次のものに変換されます。

<?hh // strict

use type Facebook\HackTest\HackTest;
use function Facebook\FBExpect\expect;

final class HackExampleTest extends HackTest {

  private Map<int, string> $m = Map{};

  public async function beforeEachTestAsync(): Awaitable<void> {
    $this->m->addAll([
      Pair{1, '1'},
      Pair{2, '2'},
      Pair{3, '3'},
    ]);
  }

  public function testShouldBeSameMap(): void {
    expect($this->m)->toBeSame(new Map([
      1 => '1',
      2 => '2',
      3 => '3',
    ]));
  }
}

HackTestの実行は、下記のコマンドで実行できます。
*現在はカバレッジ取得方法はありません。

$ hhvm ./vendor/bin/hacktest tests/

ちなみに上記のテストはerrorになります。
インスタンスが異なるためです。
これをグリーンにするには、値のテストであればdarrayなどにするといいでしょう。

  public function testShouldBeSameMap(): void {
    expect($this->m->toDArray())->toBeSame(darray[
      1 => '1',
      2 => '2',
      3 => '3',
    ]);
  }

HackTest / Assert

アサーションは前述した様に、hhvm/fbexpect](https://github.com/hhvm/fbexpect) で記述します。
PHPUnitとそんなに変わりません。
Hackのテストは基本的には === となります。
代表的なメソッドは下記の通りです。

PHPUnit FBExpect
assertEmpty toBeEmpty
assertEquals toBePHPEqual
assertSame toBeSame
assertNotSame toNotBeSame
assertTrue toBeTrue
assertFalse toBeFalse
assertNull toBeNull

前処理・後処理

setUpやtearDownの代わりになるものです

メソッド単位

asyncになりますが、\HH\Asio\join しないといけないとかはありません。

前処理

public async function beforeEachTestAsync(): Awaitable<void> {

}

後処理

public async function afterEachTestAsync(): Awaitable<void> {

}

クラス単位

最初のテストメソッド開始前に一度のみと、
最後のテストメソッド終了時に一度だけ実行されるものです。

前処理

public static async function beforeFirstTestAsync(): Awaitable<void> {

}

後処理

public static async function afterLastTestAsync(): Awaitable<void> {

}

DataProvider

書き方がHackになるだけで特に変わりません。
DataProvider Attributeを使って、テストデータとして利用するメソッドを指定します。

<?hh // strict

use type Facebook\HackTest\HackTest;
use function Facebook\FBExpect\expect;

class MyClassTest extends HackTest {

  public function fooData(): vec<(string, int)> {
    return vec[
      tuple('foo', 123),
      tuple('bar', 456),
    ];
  }

  <<DataProvider('fooData')>>
  public function testFoo(string $arg1, int $arg2) {
    // code
  }
}

ExpectedException

PHPUnitのものと同じです。
PHPUnitでは次の様になります。

use PHPUnit\Framework\TestCase;

class MyTest extends TestCase
{
    /**
     * @expectedException     MyException
     * @expectedExceptionCode 20
     */
    public function testExceptionHasErrorCode20()
    {
        throw new MyException('Some Message', 20);
    }
}

HackTestでは下記の通りです。

<?hh // strict

use type MyException;
use type Facebook\HackTest\HackTest;

final class MyTest extends HackTest
{
  <<ExpectedException(MyException::class), ExpectedExceptionCode(20)>>
  public function testExceptionHasErrorCode20(): void {
    throw new MyException('Some Message', 20);
  }
}

記述方法が変わるだけでそこまで変わりません。
次回はこれらを使って実際にユニットテストを記述してみましょう。