Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to write Jest unit test for a Vue form component which uses a Vuex store?

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 is returned
  • 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>
like image 976
sakhunzai Avatar asked May 08 '18 12:05

sakhunzai


1 Answers

Vue test utils documentation says:

[W]e recommend writing tests that assert your component's public interface, and treat its internals as a black box. A single test case would assert that some input (user interaction or change of props) provided to the component results in the expected output (render result or emitted custom events).

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:

Like mount, it creates a Wrapper that contains the mounted and rendered Vue component, but with stubbed child components.

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>
like image 167
Emile Bergeron Avatar answered Oct 02 '22 15:10

Emile Bergeron