毎日何時間か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);
});
});