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

12,186

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>
Share:
12,186
sakhunzai
Author by

sakhunzai

SOreadytohelp

Updated on July 22, 2022

Comments

  • sakhunzai
    sakhunzai almost 2 years

    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>
    
  • sakhunzai
    sakhunzai about 6 years
    thanks for you details answer. Would you mind to explain what do you mean by " scoping them to their sole responsibility " how this will work with SFC. Is this some conceptual scoping or I ve to refactor them phsically to multiple components ?
  • sakhunzai
    sakhunzai about 6 years
    Further would you describe when to use shallow/mount etc. Your comments are highly appreciated
  • JoeSchr
    JoeSchr about 4 years
    Excellent answer for something I struggled for quite a time now. All the material I read on TDD with Vue didn't explain how to handle "complexity" as much as just how to use the testing frameworks...