web-dev-qa-db-ja.com

Vuexストアを使用するVueフォームコンポーネントのJestユニットテストを作成するにはどうすればよいですか?

ログインフォームがあります。ログインフォームにデータを入力してログインボタンをクリックすると、次のようになります。

  • フォームデータ(ユーザー名、パスワード)がサーバーに送信され、応答が返されます
  • フォームデータが無効な場合、メッセージは<flash-message>コンポーネントによって表示されます
  • フォームデータが有効な場合、ユーザーはダッシュボードにリダイレクトされます

このコンポーネントはVuexストアに大きく依存しているため、このコンポーネントのいくつかの有効なテストケースを考えることができません。

  • このコンポーネントはテスト可能ですか?
  • テスト可能である場合、どうすればjestで単体テストを作成できますか?
  • コンポーネントのどの部分をモックする必要がありますか?
  • Vue-test-utilsのmount/shallowMountメソッドを使用してコンポーネントをラップする必要がありますか?
  • 私のコンポーネントはBootstrap-VueUIコンポーネントを使用しています。どうすれば対処できますか?

JavaScriptエコシステムの経験がないので、詳細に説明していただければ幸いです。

Login.vue

<template>
  <b-col sm="6" offset-sm="3">
    <h1><span class="fa fa-sign-in"></span> Login</h1>
    <flash-message></flash-message>
    <!-- LOGIN FORM -->
    <div class="form">
        <b-form-group>
            <label>Email</label>
            <input type="text" class="form-control" name="email" v-model="email">
        </b-form-group>

        <b-form-group>
            <label>Password</label>
            <input type="password" class="form-control" name="password" v-model="password">
        </b-form-group>

        <b-btn type="submit" variant="warning" size="lg" @click="login">Login</b-btn>
    </div>

    <hr>

    <p>Need an account? <b-link :to="{name:'signup'}">Signup</b-link></p>
    <p>Or go <b-link :to="{name:'home'}">home</b-link>.</p>
  </b-col>

</template>

<script>
export default {
  data () {
    return {
      email: '',
      password: ''
    }
  },
  methods: {
    async login () {
      this.$store.dispatch('login', {data: {email: this.email, password: this.password}, $router: this.$router})
    }
  }
}
</script>
6
sakhunzai

Vue test utilsのドキュメント 言います:

[W] eは、コンポーネントのパブリックインターフェイスをアサートし、その内部をブラックボックスとして扱うテストを作成することをお勧めします。単一のテストケースは、コンポーネントに提供された入力(ユーザーの操作または小道具の変更)が期待される出力(レンダリング結果または出力されたカスタムイベント)をもたらすことを表明します。

したがって、bootstrap-vueコンポーネントをテストするべきではありません。それは、そのプロジェクトのメンテナの仕事です。

単体テストを念頭に置いてコードを書く

コンポーネントのテストを容易にするために、コンポーネントをそれぞれの責任でスコープすることが役立ちます。つまり、ログインフォームは独自のSFC(単一ファイルコンポーネント)である必要があり、ログインページはログインフォームを使用する別のSFCです。

ここでは、ログインページから分離されたログインフォームがあります。

<template>
    <div class="form">
        <b-form-group>
            <label>Email</label>
            <input type="text" class="form-control" 
                   name="email" v-model="email">
        </b-form-group>

        <b-form-group>
            <label>Password</label>
            <input type="password" class="form-control" 
                   name="password" v-model="password">
        </b-form-group>

        <b-btn type="submit" variant="warning" 
               size="lg" @click="login">
               Login
        </b-btn>
    </div>
</template>

<script>
export default {
    data() {
        return { email: '', password: '' };
    },
    methods: {
        login() {
            this.$store.dispatch('login', {
                email: this.email,
                password: this.password
            }).then(() => { /* success */ }, () => { /* failure */ });
        }
    }
}
</script>

ログインが成功または失敗したときにリダイレクトを処理するのはストアの責任ではないため、ストアアクションディスパッチからルーターを削除しました。ストアは、その前にフロントエンドがあることを知る必要はありません。データとデータに関連する非同期リクエストを処理します。

各パーツを個別にテストします

ストアアクションを個別にテストします。次に、コンポーネントで完全にモックすることができます。

ストアアクションのテスト

ここでは、ストアが本来の目的を果たしていることを確認したいと思います。したがって、状態に正しいデータがあること、それらをモックしながらHTTP呼び出しが行われることを確認できます。

import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import storeConfig from '@/store/config';

describe('actions', () => {
    let http;
    let store;

    beforeAll(() => {
        http = new MockAdapter(axios);
        store = new Vuex.Store(storeConfig());
    });

    afterEach(() => {
        http.reset();
    });

    afterAll(() => {
        http.restore();
    });

    it('calls login and sets the flash messages', () => {
        const fakeData = { /* ... */ };
        http.onPost('api/login').reply(200, { data: fakeData });
        return store.dispatch('login')
            .then(() => expect(store.state.messages).toHaveLength(1));
    });
    // etc.
});

シンプルなLoginFormのテスト

このコンポーネントが行う唯一の実際のことは、送信ボタンが呼び出されたときにloginアクションをディスパッチすることです。したがって、これをテストする必要があります。アクション自体はすでに個別にテストされているため、テストする必要はありません。

import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import LoginForm from '@/components/LoginForm';

const localVue = createLocalVue();
localVue.use(Vuex);

describe('Login form', () => {

    it('calls the login action correctly', () => {
        const loginMock = jest.fn(() => Promise.resolve());
        const store = new Vuex.Store({
            actions: {
                // mock function
                login: loginMock
            }
        });
        const wrapper = mount(LoginForm, { localVue, store });
        wrapper.find('button').trigger('click');
        expect(loginMock).toHaveBeenCalled();
    });
});

フラッシュメッセージコンポーネントのテスト

同じように、挿入されたメッセージでストアの状態をモックし、各メッセージアイテム、クラスなどの存在をテストして、FlashMessageコンポーネントがメッセージを正しく表示することを確認する必要があります。

ログインページのテスト

ログインページコンポーネントを単なるコンテナにすることができるようになったため、テストする必要はほとんどありません。

<template>
    <b-col sm="6" offset-sm="3">
        <h1><span class="fa fa-sign-in"></span> Login</h1>
        <flash-message />
        <!-- LOGIN FORM -->
        <login-form />
        <hr>
        <login-nav />
    </b-col>
</template>

<script>
import FlashMessage from '@/components/FlashMessage';
import LoginForm from '@/components/LoginForm';
import LoginNav from '@/components/LoginNav';

export default {
    components: {
        FlashMessage,
        LoginForm,
        LoginNav,
    }
}
</script>

mountshallowをいつ使用するか

shallow に関するドキュメントは次のように述べています。

mount のように、マウントおよびレンダリングされたVueコンポーネントを含む Wrapper を作成しますが、スタブされた子コンポーネントを使用します。

つまり、コンテナコンポーネントの子コンポーネントは<!-- -->コメントに置き換えられ、それらのすべての対話性はそこにありません。そのため、テスト対象のコンポーネントを、その子が持つ可能性のあるすべての必要なものから分離します。

ログインページに挿入されたDOMはほとんど空になり、FlashMessageLoginForm、およびLoginNavコンポーネントが置き換えられます。

<b-col sm="6" offset-sm="3">
    <h1><span class="fa fa-sign-in"></span> Login</h1>
    <!-- -->
    <!-- LOGIN FORM -->
    <!-- -->
    <hr>
    <!-- -->
</b-col>
4
Emile Bergeron