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

# Hackでテストっでどうするの？
今回はそんなHackのユニットテストについてです。

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

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

```json: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](https://hhvm.com/blog/2018/09/12/end-of-php-support-future-of-hack.html)
このため、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](https://github.com/hhvm/hacktest)　へ移行しています。
これによってこれまでPHPUnitに依存していたものがHackのみとなっており、
PHPライブラリ依存離れが急ピッチで進んでいる訳です。
実際のアサートなどは [hhvm/fbexpect](https://github.com/hhvm/fbexpect) を利用です。

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

## HackTest

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

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

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

## PHPUnitからの移行

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

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

```hack
<?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を実行します。

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

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

```hack
<?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の実行は、下記のコマンドで実行できます。
*現在はカバレッジ取得方法はありません。

```bash
$ hhvm ./vendor/bin/hacktest tests/
```

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

```hack
  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` しないといけないとかはありません。

#### 前処理
```hack
public async　function beforeEachTestAsync(): Awaitable<void> {

}
```

### 後処理

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

}
```

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

#### 前処理
```hack
public static async function beforeFirstTestAsync(): Awaitable<void> {

}
```

### 後処理
```hack
public static async function afterLastTestAsync(): Awaitable<void> {

}
```

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

```hack
<?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では次の様になります。

```php
use PHPUnit\Framework\TestCase;

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

```

HackTestでは下記の通りです。

```hack
<?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);
  }
}

```

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