JestでYupのvalidationSchemaのテストを書く

React+Yupでフォームを作っていた際、フォームの項目が増えれば増えるほどバリデーションの抜け漏れが増え、QAで返ってくる項目が多くなってきたのでこれはよくないな…と思ったのと手作業で何個も色んな項目を都度チェックしていくのが面倒くさいなという気持ちになったので、Jestを使ってYupのバリデーションのユニットテストを書いてみました。

# サンプルコード
GitHubに実際に動作するコードをアップしてます。
https://github.com/unachang113/yup-schema-test-sample

# 1. 簡単なschemaのテスト
## 1.1 テスト対象のスキーマ
まずは以下の簡単なschemaのテストから行っていきます。

```ts:Validation.ts
import * as yup from "yup";

export const idValidationSchema = yup
  .string()
  .required("※IDは必須です")
  .min(5, "※5文字以上で入力して下さい")
  .max(10, "※10文字以下で入力して下さい");
```

## 1.2 テストケースの作成

今回の場合、以下のテスト項目が考えられます。

- IDが5文字以下の場合「※5文字以上で入力して下さい」というエラーが返ってくること
- IDが10文字以上の場合「※10文字以下で入力して下さい」というエラーが返ってくること
- IDが未入力の場合「※IDは必須です」というエラーが返ってくること

## 1.3 テストの作成

まず最初に

> IDが5文字以下の場合「5文字以上で入力して下さい」というエラーが返ってくること

こちらのバリデーションテストを書いていきます。

```ts:Validation.test.ts
import { idValidationSchema } from './Validation.ts';

it('IDが5文字以下の場合、エラー', ()=> {
	const testValue = 'hoge'; // 4文字のテキスト

	expect(() => {
      idValidationSchema.validateSync(testValue);
    }).toThrow('※5文字以上で入力して下さい');
});
```

`idValidationSchema.validateSync` はバリデーションがエラーになった場合、
ValidationErrorをthrowするので、jestのtoThrowでバリデーションメッセージが正しく返ってきているかをテストできます。

上と同様に他の項目のテストも書いていくと以下のようになります

```ts:Validation.test.ts
import { idValidationSchema } from './Validation.ts';

describe("IDのschemaテスト", () => {
    it("IDが5文字以下の場合、エラー", () => {
        const testValue = "hoge"; // 4文字のテキスト
        
        expect(() => {
          idValidationSchema.validateSync(testValue);
        }).toThrow("※5文字以上で入力して下さい");
    });
    
    it("IDが10文字以上の場合、エラー", () => {
        const testValue = "hogehogehoge"; // 12文字のテキスト
        
        expect(() => {
          idValidationSchema.validateSync(testValue);
        }).toThrow("※10文字以下で入力して下さい");
    });
    
    it("IDが空の場合、エラー", () => {
        const testValue = "";
        
        expect(() => {
          idValidationSchema.validateSync(testValue);
        }).toThrow("※IDは必須です");
    });
});
```

# 2. objectSchemaのテスト

次はobject形式のschemaのテストを書いていきます。

## 2.1 テスト対象のスキーマ

```tsx:Validation.ts
import * as yup from "yup";

export const validationSchema = yup.object().shape({
  id: yup
    .number()
    .required("※IDは必須です")
    .typeError("※IDは数値で入力して下さい")
    .min(1, "※IDは1以上で入力して下さい"),
  name: yup.string().required("※名前は必須です"),
  description: yup.string().max(140, "※説明文は140文字以下で入力して下さい"),
});
```

## 2.2 テストケースの作成

先程のschemaのテストと同様、テストケースを考えていきます。

### idの場合
- IDが数値以外の値の場合、「※IDは数値で入力してください」というエラーが返ってくること
- IDが1以下の場合、「※IDは1以上で入力して下さい」というエラーが返ってくること
- IDが未入力の場合、「※IDは必須です」というエラーが返ってくること

### nameの場合
- 名前が未入力の場合、「※名前は必須です」というエラーが返ってくること

### descriptionの場合
- 説明文が未入力の場合、エラーにならないこと
- 説明文が140文字以上の場合、「※説明文は必須です」というエラーが返ってくること

上記に加え、すべての値がエラーの場合のテストケースも定義していきます。

## 2.3 テストの作成

id,name,description単体のエラーは先程と同様 `validateSync` を用いてテストの作成が可能です。

```ts:Validation.test.ts
import { validationSchema } from './Validation.ts';

it("IDが数値以外の値の場合、エラー", () => {
    const testValues = {
      id: "",
      name: "テスト",
      description: "説明文です",
    };
    
    expect(() => {
      validationSchema.validateSync(testValues);
    }).toThrow("※IDは数値で入力して下さい");
});

it("IDが0の場合、エラー", () => {
    const testValues = {
      id: 0,
      name: "テスト",
      description: "説明文です",
    };
    
    expect(() => {
      validationSchema.validateSync(testValues);
    }).toThrow("※IDは1以上で入力して下さい");
});

it("IDが空の場合、エラー", () => {
    const testValues = {
      name: "テスト",
      description: "説明文です",
    };
    
    expect(() => {
      validationSchema.validateSync(testValues);
    }).toThrow("※IDは必須です");
});

it("名前が空の場合、エラー", () => {
    const testValues = {
      id: 123456,
      name: "",
      description: "説明文です",
    };
    
    expect(() => {
      validationSchema.validateSync(testValues);
    }).toThrow("※名前は必須です");
    });
    
    it("説明文が空の場合、正常", () => {
    const testValues = {
      id: 123456,
      name: "山田太郎",
      description: "",
    };
    
    expect(validationSchema.isValidSync(testValues)).toBe(true);
});

it("説明文が140文字以上の場合、エラー", () => {
    // 141文字のダミーテキスト作成
    const descriptionText = new Array(141).fill("あ").join("");
    const testValues = {
      id: 123456,
      name: "山田太郎",
      description: descriptionText,
    };
    
    expect(() => {
      validationSchema.validateSync(testValues);
    }).toThrow("※説明文は140文字以下で入力して下さい");
});
```

単体のエラーの場合は前述の`validateSync` で対応可能ですが、
`validateSync`は複数の項目でエラーが出ても1つのエラーしかthrowしないので、 
複数項目のエラーをテストしたい場合は`validate`を使用してテストを行っていきます。

```ts:Validation.test.ts
import { validationSchema } from './Validation.ts';

it("※IDが日本語でnameがない場合、IDとnameでエラーが返ってくる", async () => {
    const testValue = {
        id: "あいうえお",
        name: "",
        description: "",
    };

    try {
        await validationSchema.validate(testValue, { abortEarly: false }); // abortEarly: falseを設定すると各項目のエラーがすべて返ってくる
    } catch (validationError) {
        const validationErrors: Record<string, string> = {};
        const yupError = validationError as yup.ValidationError;

        yupError.inner.forEach((error) => {
            if (error.path) {
                validationErrors[error.path] = error.message;
            }
        });

        // eslint-disable-next-line jest/no-conditional-expect
        expect(validationErrors).toEqual({
            id: "※IDは数値で入力して下さい",
            name: "※名前は必須です",
        });
    }
});
```

# 参考
****[Yup と i18next を使ったスキーマの単体テストを作ってみた](https://tech.aptpod.co.jp/entry/2021/12/23/070000)****