[Vue & Vuetify] VuetifyのFormのバリデーションをテスト

2020-12-21IT記事,VueVue.js,Vuetify

毎日何時間かVueとVuetifyを個人プロジェクトで触っている。Formにバリデーションチェックをつけようと思った。

そこでまずテストを書こうと思い立つ。

以下にVuetifyでバリデーションのテストをする場合のポイントを書く。

バリデーションチェック関数のテスト

vuetifyのForm類ではrulesというプロパティからバリデーションチェックの関数を設定できる。

        <v-textarea
          :rules="rules"
          v-model="formText"
        ></v-textarea>
 
...

      rules: [
        (v) => !!v || "入力してください",
        (v) => v == null || v.length <= 200 || "最大200文字まで",
      ],

上記では入力してあるのかどうかと、文字数制限を行っている。

Javascriptの || は左側がtrueになるとその場でtrueが返り、falseになると右側が返る。これでバリデーションの判定を行い、有効な値の判定をすり抜けていくと、エラー文字列が返るようになっている。ようは型が(null | any -> Boolean | String)になればよい。

このruleからテストする。

  describe("rules", () => {
    it("一文字の場合有効", () => {
      let actual = wrapper.vm.rules[0]("a");
      expect(actual).toBe(true);
    });

    it("空文字の場合、エラー表示が返る", () => {
      let actual = wrapper.vm.rules[0]("");
      expect(typeof actual).toBe("string");
    });

    it("nullの場合、エラー表示が返る", () => {
      let actual = wrapper.vm.rules[0](null);
      expect(typeof actual).toBe("string");
    });

    it("200文字のとき、有効", () => {
      let actual = wrapper.vm.rules[1]("a".repeat(200));
      expect(actual).toBeTruthy();
    });
    it("201文字だと、エラー表示が返る", () => {
      let actual = wrapper.vm.rules[1]("a".repeat(201));
      expect(typeof actual).toBe("string");
    });

    it("nullでも、エラーがでない", () => {
      let actual = wrapper.vm.rules[1](null);
      expect(actual).toBe(true);
    });
  });

最後のが少し違うが、 バリデーションチェック(rule)の2つ目

(v) => v == null || v.length <= 200 || "最大200文字まで",

このvがnullになることがあり、nullになると.lengthでエラーが出ちゃうので、nullを有効扱いにしている。(nullチェック自体はruleの1つ目でしている)

Formに入力して、バリデーションが想定通り起動しているかのテスト

次にVuetifyのFormに入力して、決定ボタンなどを押した後にFormにエラーが表示されているか、というテストを書いてみた。

まずForm部分とクリック部分

      <v-form ref="create_todo_form" v-model="isValid" lazy-validation>
        <v-textarea
          ref="title"
          solo
          counter
          :rules="rules"
          v-model="formText"
        ></v-textarea>
      </v-form>

      <v-btn color="primary" @click.stop="onClickSubmit">
        新しいTodoを書く
      </v-btn>

....

    isValid : true ,  // v-formのバリデーションの状態

....

    // 新しいTodoを送信
    onClickSubmit() {
      // バリデーションエラーだと何も起きない
      if (
        !this.$refs["create_request_form"].validate() // バリデーションチェックを行う。エラーならfalseが返る
      ) {
        console.log("バリデーションエラー!");
        return;
      }
      this.postNewRequest();
    },

まずポイントがいくつかあって、

・v-textareaをv-formでくるむようにする。

・v-formにv-modelを付けておく。上記ではisValidという値を紐付けた。v-formにv-modelを付けておくと、v-formのvalueという値が変わるようになる。テストで使う。その値はエラー表示時にfalseになる。

・v-formにはvalidate()やreset()関数がある。ここではvalidate()を使ってバリデーションを行い結果で分岐するようにしている。reset()は文字列入力の値が空文字になり、ほかの入力はnullになる。

・refなどを付けておくと便利。

そしてテスト部分

   wrapper = mount(CreateRequestForm, { ...
    });
 
   it("入力してボタンを押したとき、バリデーションエラーだとエラーを表示する", async () => {
    await wrapper
      .findComponent({ ref: "title" })
      .find("textarea")
      .setValue("");
    await wrapper.find("button").trigger("click");
    await wrapper.vm.$nextTick();
    expect(wrapper.vm.isValid).toBe(
      false
    );
  });

・まずshallowMountではなく、mountを使う。shallowMountだと、inputコンポーネントなどがスタブになってsetValueが呼べなくなる。(setValueは値を設定してイベントを起こしてくれる便利関数)

・findComponentで入力部分のコンポーネントをとって、中のinputか、textareaを取り出す。それからsetValue関数を呼ぶ。

・setValueやtrigger(“click")したあとは、await wrapper.vm.$nextTick()などで待つ。

・バリデーションエラーになっているかはwrapper.vm.isValidまたはfom.vm.valueでわかる。

感想

書いてみると落とし穴多いね!?後半しなくていい苦労な気もするんだけど(vuetifyをテストしたいわけではない)、Form自体をコンポーネントに切り出したらこういう形になってしまった。いろいろ書いてみてテスト力を鍛えていこうと思う。

テスト全部を載せておきます。(aws-amplifyのモックとかあってむちゃくちゃやけど……。)

import { createLocalVue, mount } from "@vue/test-utils";
import CreateRequestForm from "../CreateRequestForm.vue";
import Vuex from "vuex";
const localVue = createLocalVue();
localVue.use(Vuex);

let requestBoardMock = {
  createdAt: "2020-12-11T18:59:25.703Z",
  description: null,
  id: "aaaaaaaa",
  owner: "951885bb-d3d6-4de5-b0b2-18237af0e2e7",
  title: "aaa ",
  list: { items: [] },
  type: "b",
  updatedAt: "2020-12-11T18:59:25.703Z",
  url: null,
};

jest.mock("aws-amplify", () => {
  return {
    API: {
      graphql: jest.fn(() => {
        return {
          data: {
            createRequest: { id: "aaaa", title: "helohelo", like: 0 },
          },
        };
      }),
    },
  };
});

describe("RequestBoarForm", () => {
  let wrapper;
  beforeAll(() => {
    wrapper = mount(CreateRequestForm, {
      localVue,
      store: new Vuex.Store({
        getters: { isSignIn: () => true },
      }),
      propsData: {
        requestBoard: requestBoardMock,
        dialog: true,
      },
    });
  });

  describe("rules", () => {
    it("一文字の場合正常", () => {
      let actual = wrapper.vm.rules[0]("a");
      expect(actual).toBe(true);
    });

    it("何も書いていない場合、エラー表示が返る", () => {
      let actual = wrapper.vm.rules[0]("");
      expect(typeof actual).toBe("string");
    });

    it("nullの場合、エラー表示が返る", () => {
      let actual = wrapper.vm.rules[0](null);
      expect(typeof actual).toBe("string");
    });

    it("200文字のとき、正常", () => {
      let actual = wrapper.vm.rules[1]("a".repeat(200));
      expect(actual).toBeTruthy();
    });
    it("201文字だと、エラー表示が返る", () => {
      let actual = wrapper.vm.rules[1]("a".repeat(201));
      expect(typeof actual).toBe("string");
    });

    it("nullでも、エラーがでない", () => {
      let actual = wrapper.vm.rules[1](null);
      expect(actual).toBe(true);
    });
  });

  it("入力してボタンを押したとき、さらに入力内容が正常で、さらにapiを呼んで成功だと、フォームをクリアして閉じる", async () => {
    await wrapper
      .findComponent({ ref: "title" })
      .find("textarea")
      .setValue("title");
    await wrapper.find("button").trigger("click");
    await wrapper.vm.$nextTick();
    expect(wrapper.vm.requestBoard.list.items.length).toEqual(1);
    expect(wrapper.emitted("update:requestBoard")).toBeTruthy();
    expect(wrapper.emitted("update:dialog")).toBeTruthy();
    await wrapper.vm.$nextTick();
    expect(
      wrapper.findComponent({ ref: "title" }).find("textarea").element.value
    ).toBe("");
  });

  it("入力してボタンを押したとき、バリデーションエラーだとエラーを表示する", async () => {
    await wrapper
      .findComponent({ ref: "title" })
      .find("textarea")
      .setValue("");
    await wrapper.find("button").trigger("click");
    await wrapper.vm.$nextTick();
    expect(wrapper.findComponent({ ref: "create_request_form" }).vm.value).toBe(
      false
    );
  });

  it("要望入力で200文字より上だとエラー表示", async () => {
    await wrapper
      .findComponent({ ref: "title" })
      .find("textarea")
      .setValue("a".repeat(201));
    await wrapper.vm.$nextTick();
    expect(wrapper.findComponent({ ref: "create_request_form" }).vm.value).toBe(
      false
    );
  });

  it("要望入力で200文字だと正常", async () => {
    await wrapper
      .findComponent({ ref: "title" })
      .find("textarea")
      .setValue("a".repeat(200));
    await wrapper.vm.$nextTick();
    let form = wrapper.findComponent({ ref: "create_request_form" });
    expect(form.vm.value).toBe(true);
  });

});