本文介绍了如何为使用 Vuex 存储的 Vue 表单组件编写 Jest 单元测试?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个登录表单.当我用数据填写登录表单并点击登录按钮时:

  • 表单数据(用户名、密码)被发送到服务器,响应是返回
  • 如果表单数据无效,组件会显示一条消息
  • 如果表单数据有效,用户将被重定向到仪表板

由于这个组件严重依赖于 Vuex 商店,我无法为这个组件想到一些有效的测试用例.

  • 该组件是否可测试?
  • 如果它是可测试的,我该如何用 Jest 编写单元测试?
  • 我应该模拟组件的哪些部分?
  • 我应该使用 vue-test-utils mount/shallowMount 方法来包装我的组件吗?
  • 我的组件使用 Bootstrap-Vue UI 组件.我该如何处理?

我没有 JavaScript 生态系统方面的经验,因此如果能提供详细的解释,我们将不胜感激.

登录.vue

<b-col sm="6" offset-sm="3"><h1><span class="fa fa-sign-in"></span>登录<flash-message></flash-message><!-- 登录表单--><div class="form"><b-form-group><label>电子邮件</label><input type="text" class="form-control" name="email" v-model="email"></b-form-group><b-form-group><label>密码</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">登录</b-btn>

<小时><p>需要一个帐户?<b-link :to="{name:'signup'}">注册</b-link></p><p>或者去<b-link :to="{name:'home'}">home</b-link>.</p></b-col><脚本>导出默认{数据 () {返回 {电子邮件: '',密码: ''}},方法: {异步登录(){this.$store.dispatch('login', {data: {email: this.email, password: this.password}, $router: this.$router})}}}

解决方案

Vue 测试工具文档 说:

[W]e 建议编写测试来断言组件的公共接口,并将其内部视为一个黑匣子.单个测试用例会断言提供给组件的某些输入(用户交互或道具更改)会产生预期的输出(渲染结果或发出的自定义事件).

所以我们不应该测试 bootstrap-vue 组件,这是项目维护者的工作.

在编写代码时考虑单元测试

为了更容易地测试组件,将它们限定为它们的唯一职责会有所帮助.意思是登录表单应该是自己的SFC(单文件组件),登录页面是另一个使用登录表单的SFC.

在这里,我们将登录表单与登录页面隔离开来.

<div class="form"><b-form-group><label>电子邮件</label><input type="text" class="form-control"name="email" v-model="email"></b-form-group><b-form-group><label>密码</label><input type="password" class="form-control"名称="密码" v-model="密码"></b-form-group><b-btn type="提交"变体="警告"size="lg" @click="登录">登录</b-btn>

<脚本>导出默认{数据() {返回{电子邮件:'',密码:''};},方法: {登录() {this.$store.dispatch('登录', {电子邮件:this.email,密码:this.password}).then(() => {/* 成功 */}, () => {/* 失败 */});}}}

我从商店操作分派中删除了路由器,因为当登录成功或失败时,商店不负责处理重定向.商店不应该知道它前面有一个前端.它处理与数据相关的数据和异步请求.

独立测试每个部分

单独测试商店操作.然后可以在组件中完全模拟它们.

测试商店操作

在这里,我们要确保商店按其规定执行.因此,我们可以检查状态是否具有正确的数据,是否在模拟它们时进行了 HTTP 调用.

从'vuex'导入Vuex;从 'axios' 导入 axios;从 'axios-mock-adapter' 导入 MockAdapter;从 '@/store/config' 导入 storeConfig;描述('动作',()=> {让http;让商店;beforeAll(() => {http = 新 MockAdapter(axios);store = new Vuex.Store(storeConfig());});afterEach(() => {http.reset();});afterAll(() => {http.restore();});it('调用登录并设置闪现信息', () => {const fakeData = {/* ... */};http.onPost('api/login').reply(200, { data: fakeData });返回 store.dispatch('登录').then(() => expect(store.state.messages).toHaveLength(1));});//等等.});

测试我们简单的登录表单

这个组件唯一真正做的就是在调用提交按钮时分派 login 操作.所以我们应该测试一下.我们不需要测试动作本身,因为它已经单独测试过.

从'vuex'导入Vuex;import { mount, createLocalVue } from '@vue/test-utils';从 '@/components/LoginForm' 导入登录表单;const localVue = createLocalVue();localVue.use(Vuex);describe('登录表单', () => {it('正确调用登录操作', () => {const loginMock = jest.fn(() => Promise.resolve());const store = new Vuex.Store({动作:{//模拟函数登录: loginMock}});const wrapper = mount(LoginForm, { localVue, store });wrapper.find('button').trigger('click');期望(登录模拟).toHaveBeenCalled();});});

测试 flash 消息组件

同样,我们应该用注入的消息模拟存储状态,并通过测试每个消息项、类等的存在来确保 FlashMessage 组件正确显示消息.

测试登录页面

登录页面组件现在可以只是一个容器,因此不需要太多测试.

<b-col sm="6" offset-sm="3"><h1><span class="fa fa-sign-in"></span>登录<闪现消息/><!-- 登录表格--><登录表单/><小时><登录导航/></b-col><脚本>从 '@/components/FlashMessage' 导入 FlashMessage;从 '@/components/LoginForm' 导入登录表单;从 '@/components/LoginNav' 导入 LoginNav;导出默认{成分: {快讯,登录表格,登录导航,}}

何时使用mount vs shallow

关于shallow的文档 说:

mount,它创建了一个 Wrapper ,其中包含挂载并呈现的 Vue 组件,但带有存根子组件.

意味着来自容器组件的子组件将被替换为 <!-- --> 注释,并且它们的所有交互性都将不存在.因此,它将被测试的组件与其子项可能具有的所有要求隔离开来.

登录页面的插入 DOM 将几乎为空,其中 FlashMessageLoginFormLoginNav 组件将被替换:

<h1><span class="fa fa-sign-in"></span>登录<!-- --><!-- 登录表格--><!-- --><小时><!-- --></b-col>

I have a login form. When I fill out the login form with data and the login button is clicked:

  • form data (username, password) is sent to the server and a response isreturned
  • If the form data is invalid, a message is displayed by the <flash-message> component
  • If the form data is valid, the user is redirected to the dashboard

Since this component heavily depends on the Vuex store, I'm unable to think of some valid test cases for this component.

  • Is this component testable?
  • If it is testable, how do I write a unit test in jest?
  • Which part(s) of my component should I mock?
  • Should I use the vue-test-utils mount/shallowMount methods to wrap my component?
  • My component uses Bootstrap-Vue UI components. How do I deal with them?

I don't have experience with JavaScript ecosystem, so a verbose explanation would be appreciated.

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>
解决方案

Vue test utils documentation says:

So we shouldn't be testing bootstrap-vue components, that's the job of that project's maintainers.

Write code with unit tests in mind

To make it easier to test components, scoping them to their sole responsibility will help. Meaning that the login form should be its own SFC (single file component), and the login page is another SFC that uses the login form.

Here, we have the login form isolated from the login page.

<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>

I removed the router from the store action dispatch as it's not the store responsibility to handle the redirection when the login succeeds or fails. The store shouldn't have to know that there's a frontend in front of it. It deals with the data and async requests related to the data.

Test each part independently

Test the store actions individually. Then they can be mocked completely in components.

Testing the store actions

Here, we want to make sure the store does what it's meant to do. So we can check that the state has the right data, that HTTP calls are made while mocking them.

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.
});

Testing our simple LoginForm

The only real thing this component do is dispatching the login action when the submit button is called. So we should test this. We don't need to test the action itself since it's already tested individually.

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();
    });
});

Testing the flash message component

In that same vein, we should mock the store state with injected messages and make sure that the FlashMessage component displays the messages correctly by testing the presence of each message items, the classes, etc.

Testing the login page

The login page component can now be just a container, so there's not much to test.

<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>

When to use mount vs shallow

The documentation on shallow says:

Meaning that child components from a container component will be replaced with <!-- --> comments and all their interactivity won't be there. So it isolates the component being tested from all the requirements its children may have.

The inserted DOM of the login page would then be almost empty, where the FlashMessage, LoginForm and LoginNav components would be replaced:

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

这篇关于如何为使用 Vuex 存储的 Vue 表单组件编写 Jest 单元测试?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-12 14:16