AdonisJS(アドニスJS)でユニットテストをやってみた

# はじめに
今までテストコードを書いたことがほとんどなかったのですが、テストコードを書く機会があったので調べながら実装してみました。

今回は、AdonisJSでユニットテストを書いてみます。
AdonisJSって何？って思った方は[こちらの記事](https://qiita.com/namasa/items/b17bfc1900750b9fae0b)を読んでいただければと思います:smile:

:::note info
テストは基本的に以下の3つに分かれます
- ユニットテスト
- インテグレーションテスト
- 機能テスト(フィーチャーテスト、E2Eテストともいう)
:::


# やってみた
では、さっそくやってみます！
AdonisJSでは、`Japa`というNode.js用のテストフレームワークを使用します。

## テストの設定
まず、テストをするための設定からです。
`adonisrc.ts`でユニットテストと機能テストで、どのディレクトリ内のファイルのテストを実行するか定義出来ます。
```typescript:adonisrc.ts
  tests: {
    suites: [
      {
        files: ['tests/unit/**/*.spec(.ts|.js)'],
        name: 'unit',
        timeout: 2000,
      },
      {
        files: ['tests/functional/**/*.spec(.ts|.js)'],
        name: 'functional',
        timeout: 30000,
      },
    ],
    forceExit: false,
  },
```

`tests/bootstrap.ts`で、
- **configureSuite**：テスト実行時に行うsuiteレベルのフック
- **runnerHooks**：全てのテスト前後でグローバルに行うアクション
- **reporters**：テストの進行状況を報告/表示
を設定できます。

```typescript:tests/bootstrap.ts
export const configureSuite: Config['configureSuite'] = (suite) => {
  if (['browser', 'functional', 'e2e'].includes(suite.name)) {
    return suite.setup(() => testUtils.httpServer().start())
  }
}

export const runnerHooks: Required<Pick<Config, 'setup' | 'teardown'>> = {
  setup: [
    () => {
      console.log('test start')
    }
  ],
  teardown: [
    () => {
      console.log('test end')
    }
  ],
}

export const reporters: Config['reporters'] = {
  activated: ['spec'] // spec, dot, ndjson, githubを設定できる
}
```

## テストコードを書いてみる
今回は以下のバリデーションクラスのユニットテストを書いてみます。

```typescript:AdministratorValidator.ts
import vine from '@vinejs/vine'

const common = {
  username: vine.string().trim().minLength(6).maxLength(63).alphaNumeric({
    allowDashes: true,
    allowSpaces: false,
    allowUnderscores: false,
  }),
  password: vine.string().trim().minLength(8).maxLength(32),
  name: vine.string().trim().minLength(6).maxLength(127),
  roleId: vine.string().uuid(),
}

/**
 * Validates the administrator's creation action
 */
export const createAdministratorValidator = vine.compile(
  vine.object({
    name: common.name,
    username: common.username,
    password: common.password,
    roleId: common.roleId,
  })
)
```

まず、テストファイルをコマンドで作ります。
`--suite`でユニットテストを作るか機能テストを作るか指定します。
```bash:テストの作成
node ace make:test validators/AdministratorValidator.ts --suite=unit
# tests/unit/validators/administrator_validator.spec.ts

# node ace make:test controllers/AdministratorController.ts --suite=functional
# tests/functional/validators/administrator_validator.spec.ts
```

作成したユニットテストのファイルを以下のように修正し、正常系と異常系の1パターンずつ検証するテストコードを実装します。
※実際は、網羅的にやる必要があります

```typescript:administrator_validator.spec.ts
import { Assert } from '@japa/assert';
import { errors, VineValidator } from '@vinejs/vine';
import { SchemaTypes } from '@vinejs/vine/types';

/**
 *
 * @param {Assert} assert
 * @param {VineValidator<SchemaTypes, object>} validator
 * @param {object} requestData
 * @param {string} errorMessage
 * @returns {unknown}
 */
export async function assertValidationError(
  assert: Assert,
  validator: VineValidator<SchemaTypes, object>,
  requestData: object,
  errorMessage: string = 'バリデーションエラーにならずに、正常終了しました'
) {
  try {
    await validator.validate(requestData);
    assert.fail(errorMessage); // バリデーションエラーにならない場合は、テスト失敗
  } catch (error) {
    assert.isTrue(error instanceof errors.E_VALIDATION_ERROR);

    // テストしたパラメータが全てバリデーションエラーとなることを確認
    const errorFields = error.messages.map((err: any) => err.field);
    assert.includeMembers(errorFields, Object.keys(requestData));

    return error;
  }
};
```

## 実行してみる
では、テストコードが書けたので実行してみましょう。
```bash
node ace test

# 機能テストの実行
# node ace test functional

# ユニットテストの実行
# node ace test unit
```

実行すると、全てのテスト(2件)が成功していることがわかります。
```bash
[ info ] booting application to run tests...
test start

unit / 管理者登録バリデーション (tests/unit/app/validators/administrator_validator.spec.ts)
  ✔ 【正常系】文字列が下限の場合 (5.64ms)
  ✔ 【バリデーションエラー】必須項目が空文字の場合 (4.43ms)
test end

 PASSED 

Tests  2 passed (2)
 Time  160ms
```

ちなみに、失敗すると以下のようになります。
どこのチェックで失敗しているかも表示してくれますね:smile:
```
[ info ] booting application to run tests...
test start

unit / 管理者登録バリデーション (tests/unit/app/validators/administrator_validator.spec.ts)
  ✔ 【正常系】文字列が下限の場合 (4.51ms)
  ✖ 【バリデーションエラー】必須項目が空文字の場合 (8.33ms)
test end
────────────────────────────────────────────────────────────────────────────────────────────────── ERRORS ─────────────────────────────────────────────────────────────────────────────────────────────────────

❯ 管理者登録バリデーション / 【バリデーションエラー】必須項目が空文字の場合

- Expected  - 2
+ Received  + 1

  Array [
-   "username",
-   "password",
    "name",
+   "password",
    "roleId",
  ]

ℹ AssertionError: expected [ 'name', 'password', 'roleId' ] to be a superset of [ Array(4) ]

 ⁃ at Assert.includeMembers (/workspace/node_modules/@japa/assert/build/index.js:1168:19)
 ⁃ at Object.executor (tests/unit/app/validators/administrator_validator.spec.ts:48:14)

   43 ┃      } catch (error) {
   44 ┃        assert.isTrue(error instanceof errors.E_VALIDATION_ERROR);
   45 ┃  
   46 ┃        // テストしたパラメータが全てバリデーションエラーとなることを確認
   47 ┃        const errorFields = error.messages.map((err: any) => err.field);
 ❯ 48 ┃        assert.includeMembers(errorFields, Object.keys(requestData));
   49 ┃  
   50 ┃        return error;
   51 ┃      }
   52 ┃      });
   53 ┃  });
─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[1/1]─

 FAILED 

Tests  1 passed, 1 failed (2)
 Time  135ms
```


# おわりに
今回は、AdonisJSでユニットテストを書いてみました。
次は、モックを使ったユニットテストを書いてみようと思います！