What's Vue Test Utils? How to get started with Vue testing?

What's Vue Test Utils? How to get started with Vue testing?

In this tutorial we discuss vue testing, how to get started with testing and what vue test utils is. We look at creating mocks, and we look at the wrapper function.

In this tutorial we discuss vue testing, how to get started with testing and what vue test utils is. We look at creating mocks, and we look at the wrapper function.

Vue.js Testing Guide: Unit testing in Vue and Mocha

Vue.js Testing Guide: Unit testing in Vue and Mocha

Testing in Vue is really simple! In this post, you'll learn a basic unit test using Mocha and Vue.js

This document aims to outline some short examples of how to unit test using a Vue project. This guide is specifically designed to be used on the following Vue set up.

What project setup? What packages are needed?

With the above setup, you can now start writing Mocha unit tests in Vue. The steps below show how to create tests.

Creating test files

Files that are picked for the testing end with **_.spec.js_**. You can see how this is set or change this inside **_package.json_**.

package.json

"scripts": {
    "test:unit": "vue-cli-service test:unit src/**/*.spec.js"
  }
How to run tests?

In the terminal type:

npm run test:unit

Let's see how a basic unit test file will look…

import Spinner from "@/ui/Spinner";
import AppLoadingScreen from "./AppLoadingScreen";
import { shallowMount } from "@vue/test-utils";
import { expect } from "chai";

describe("AppLoadingScreen", () => {
  let component;

  beforeEach(() => {
    component = shallowMount(AppLoadingScreen);
  });

  it("should render Spinner on mount", () => {
    expect(component.find(Spinner).exists()).to.be.true;
  });
});

The numbers below reference the code AppLoadingScreen.spec.js code block above.

  1. A component spinner is imported
  2. AppLoadingScreen.vue (the Vue component we are testing is imported)
  3. **_shallowMount_**is imported from Vue utils. It creates a [**_Wrapper_**](https://vue-test-utils.vuejs.org/api/wrapper/)that contains the mounted and rendered Vue component, but with stubbed child components. A stubbed child component is a replacement for a child component rendered by the component under test. Some more details here on how stubbed components work: https://stackoverflow.com/questions/52962489/what-are-stubbed-child-components-in-vue-test-utils
  4. **_expect_** is imported from chai. expect is a chainable language to construct assertions. Assertions are used to test a specific thing in the code. Some examples of what can be chained to expect to test https://www.chaijs.com/api/bdd/

6. **_describe_**is used to outline what you are testing. This will also show in the terminal after running the test. In this case, we are testing the appLoadingScreen component.

9. **_beforeEach()_** is a Mocha method which executes the callback argument before each of the tests. We run **_shallowMount()_** inside **_beforeEach()_** so a component is mounted before every test.

10. **_shallowMount()_** method is used placing our test component inside.

13. **_it_** is where you perform individual tests. You should be able to describe the tests, in our case “it should render Spinner on mount”. This clearly outlines what the test will be.

14. **_expect_** from chai. Asserts that something should be tested. In our case we look inside our component for the Spinner component, by using **_find_** from vue utils. Find returns a wrapper of the first DOM node or Vue component matching selector. As we are using chai expect, we can chain keywords. In this case, we add **_to.be.true_**

Running the above test **_npm run test:unit_**

Displays the results of the test describe being the “AppLoadingScreen” and it being “should render Spinner on mount”.

How we would go about testing when using Vuex.

A **_.spec.js_** file that uses vuex

import Vuex from "vuex";
import { createLocalVue, shallowMount } from "@vue/test-utils";
import chai, { expect } from "chai";
import sinon from "sinon";
import sinonChai from "sinon-chai";
import Modal from "@/ui/Modal";
import BaseButton from "@/ui/BaseButton";

chai.use(sinonChai);

const localVue = createLocalVue();
localVue.use(Vuex);
localVue.component("BaseButton", BaseButton);

describe("Modal", () => {
  let store;
  const getters = {
    isModalOpen: () => true,
    activeModalName: () => "baz"
  };
  const actions = {
    TOGGLE_MODAL: () => true
  };
  let component;
  const mockMethod = sinon.spy();

  beforeEach(() => {
    store = new Vuex.Store({
      getters,
      actions
    });

    component = shallowMount(Modal, {
      store,
      localVue,
    });
  });

  describe("can close when", () => {
    it("clicking 'x'", () => {
      component.setMethods({ TOGGLE_MODAL: mockMethod });
      component.find(".modal-close-btn").trigger("click");
      expect(mockMethod).to.have.been.called.calledWith({
        isOpen: false,
        name: null
      });
    });
  });
});

The numbers below reference the code Modal.spec.js code block above.

  1. We import vuexas we are going to create a store inside the test file.
  2. createLocalVue from vue utils. Is used to create a local class of vue so we can use components, plugins and mixins without polluting the global vue class.

4. sinon is imported. sinon allows you to create test doubles. For example mock methods from the component we want to pull into the test.

5. sinon-chai is imported. Extends Chai with assertions for the Sinon.JS mocking framework.

6. The modal component is imported (the component we are testing)

7. BaseButton is imported — we have to import this component as it is registered globally.

8. chai.use is called and set to use sinonChai

11. createLocalVue is stored in a variable for reuse.

12. Set the localVueto use vuex.

13. Register the BaseButton component as it was previously registered globally.

16. Create the store variable.

17. Create the getters, matching the component getters. ( here we can change the values to help us test, for example, if something is hidden in v-if we can change to true in the test so it becomes available)

18. Create the actions, matching the component.

25. store a mockMethod using sinon.spy(). A spy call is an object representation of an individual call to a spied function, which could be a fake, spy, stub or mock method.

28. A new store is created passing the getters and actions.

33. We create the shallow mount version of the component with the localVue and store passed in.

41. setMethods allows you to use methods from the store, in this case, we are using the action we created, matching the component file action TOGGLE_MODAL. https://vue-test-utils.vuejs.org/api/wrapper-array/#setmethods

42. find is used to search for a selector in the component. From vue Utils https://vue-test-utils.vuejs.org/api/wrapper/#find

42. trigger is used to pass in the event, in this case ‘click’ is the event that would fire on this selector. https://vue-test-utils.vuejs.org/api/wrapper/trigger.html

43. Here is the test. We use expect to assert that the mockmethod to.have.been.called.calledWith chainable methods come from chai and can be used to test a variety of scenarios. calledWith can check the see what arguments have been passed in, this way we can be sure that what gets passed in does not change otherwise the test will fail and indicate why.

Summary

That’s a basic unit test using mocha and Vue.

As you can imagine each component may have different logic in with new test syntax needed. Vuex is commonly used in Vue apps, which adds another layer of complexity when testing.

With this test, we can check that a store action was called, by testing what would happen if we triggered a click event on a selector.

The store logic itself can be tested independently on its own, which would give you a detailed test of what each action and mutation are doing.

How to build Vue.js JWT Authentication with Vuex and Vue Router

How to build Vue.js JWT Authentication with Vuex and Vue Router

In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication

In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication. I will show you:

  • JWT Authentication Flow for User Signup & User Login
  • Project Structure for Vue.js Authentication with Vuex & Vue Router
  • How to define Vuex Authentication module
  • Creating Vue Authentication Components with Vuex Store & VeeValidate
  • Vue Components for accessing protected Resources
  • How to add a dynamic Navigation Bar to Vue App

Let’s explore together.

Contents

Overview of Vue JWT Authentication example

We will build a Vue application in that:

  • There are Login/Logout, Signup pages.
  • Form data will be validated by front-end before being sent to back-end.
  • Depending on User’s roles (admin, moderator, user), Navigation Bar changes its items automatically.

Screenshots

– Signup Page:

– Login Page & Profile Page (for successful Login):

– Navigation Bar for Admin account:

Demo

This is full Vue JWT Authentication App demo (with form validation, check signup username/email duplicates, test authorization with 3 roles: Admin, Moderator, User). In the video, we use Spring Boot for back-end REST APIs.

Flow for User Registration and User Login

For JWT Authentication, we’re gonna call 2 endpoints:

  • POST api/auth/signup for User Registration
  • POST api/auth/signin for User Login

You can take a look at following flow to have an overview of Requests and Responses Vue Client will make or receive.

Vue Client must add a JWT to HTTP Authorization Header before sending request to protected resources.

Vue App Component Diagram with Vuex & Vue Router

Now look at the diagram below.

Let’s think about it.

– The App component is a container with Router. It gets app state from Vuex store/auth. Then the navbar now can display based on the state. App component also passes state to its child components.

Login & Register components have form for submission data (with support of vee-validate). We call Vuex store dispatch() function to make login/register actions.

– Our Vuex actions call auth.service methods which use axios to make HTTP requests. We also store or get JWT from Browser Local Storage inside these methods.

Home component is public for all visitor.

Profile component get user data from its parent component and display user information.

BoardUser, BoardModerator, BoardAdmin components will be displayed by Vuex state user.roles. In these components, we use user.service to get protected resources from API.

user.service uses auth-header() helper function to add JWT to HTTP Authorization header. auth-header() returns an object containing the JWT of the currently logged in user from Local Storage.

Technology

We will use these modules:

  • vue: 2.6.10
  • vue-router: 3.0.3
  • vuex: 3.0.1
  • axios: 0.19.0
  • vee-validate: 2.2.15
  • bootstrap: 4.3.1
  • vue-fontawesome: 0.1.7
Project Structure

This is folders & files structure for our Vue application:

With the explaination in diagram above, you can understand the project structure easily.

Setup Vue App modules

Run following command to install neccessary modules:

npm install vue-router
npm install vuex
npm install [email protected]
npm install axios
npm install bootstrap jquery popper.js
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome

After the installation is done, you can check dependencies in package.json file.

"dependencies": {
  "@fortawesome/fontawesome-svg-core": "^1.2.25",
  "@fortawesome/free-solid-svg-icons": "^5.11.2",
  "@fortawesome/vue-fontawesome": "^0.1.7",
  "axios": "^0.19.0",
  "bootstrap": "^4.3.1",
  "core-js": "^2.6.5",
  "jquery": "^3.4.1",
  "popper.js": "^1.15.0",
  "vee-validate": "^2.2.15",
  "vue": "^2.6.10",
  "vue-router": "^3.0.3",
  "vuex": "^3.0.1"
},

Open src/main.js, add code below:

import Vue from 'vue';
import App from './App.vue';
import { router } from './router';
import store from './store';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import VeeValidate from 'vee-validate';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import {
  faHome,
  faUser,
  faUserPlus,
  faSignInAlt,
  faSignOutAlt
} from '@fortawesome/free-solid-svg-icons';

library.add(faHome, faUser, faUserPlus, faSignInAlt, faSignOutAlt);

Vue.config.productionTip = false;

Vue.use(VeeValidate);
Vue.component('font-awesome-icon', FontAwesomeIcon);

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

You can see that we import and apply in Vue object:
store for Vuex (implemented later in src/store)
router for Vue Router (implemented later in src/router.js)
bootstrap with CSS
vee-validate
vue-fontawesome for icons (used later in nav)

Create Services

We create two services in src/services folder:


services

auth-header.js

auth.service.js (Authentication service)

user.service.js (Data service)


Authentication service

The service provides three important methods with the help of axios for HTTP requests & reponses:

  • login(): POST {username, password} & save JWT to Local Storage
  • logout(): remove JWT from Local Storage
  • register(): POST {username, email, password}
import axios from 'axios';

const API_URL = 'http://localhost:8080/api/auth/';

class AuthService {
  login(user) {
    return axios
      .post(API_URL + 'signin', {
        username: user.username,
        password: user.password
      })
      .then(this.handleResponse)
      .then(response => {
        if (response.data.accessToken) {
          localStorage.setItem('user', JSON.stringify(response.data));
        }

        return response.data;
      });
  }

  logout() {
    localStorage.removeItem('user');
  }

  register(user) {
    return axios.post(API_URL + 'signup', {
      username: user.username,
      email: user.email,
      password: user.password
    });
  }

  handleResponse(response) {
    if (response.status === 401) {
      this.logout();
      location.reload(true);

      const error = response.data && response.data.message;
      return Promise.reject(error);
    }

    return Promise.resolve(response);
  }
}

export default new AuthService();

If login request returns 401 status (Unauthorized), that means, JWT was expired or no longer valid, we will logout the user (remove JWT from Local Storage).

Data service

We also have methods for retrieving data from server. In the case we access protected resources, the HTTP request needs Authorization header.

Let’s create a helper function called authHeader() inside auth-header.js:

export default function authHeader() {
  let user = JSON.parse(localStorage.getItem('user'));

  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken };
  } else {
    return {};
  }
}

It checks Local Storage for user item.
If there is a logged in user with accessToken (JWT), return HTTP Authorization header. Otherwise, return an empty object.

Now we define a service for accessing data in user.service.js:

import axios from 'axios';
import authHeader from './auth-header';

const API_URL = 'http://localhost:8080/api/test/';

class UserService {
  getPublicContent() {
    return axios.get(API_URL + 'all');
  }

  getUserBoard() {
    return axios.get(API_URL + 'user', { headers: authHeader() });
  }

  getModeratorBoard() {
    return axios.get(API_URL + 'mod', { headers: authHeader() });
  }

  getAdminBoard() {
    return axios.get(API_URL + 'admin', { headers: authHeader() });
  }
}

export default new UserService();

You can see that we add a HTTP header with the help of authHeader() function when requesting authorized resource.

Define Vuex Authentication module

We put Vuex module for authentication in src/store folder.


store

auth.module.js (authentication module)

index.js (Vuex Store that contains also modules)


Now open index.js file, import auth.module to main Vuex Store here.

import Vue from 'vue';
import Vuex from 'vuex';

import { auth } from './auth.module';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    auth
  }
});

Then we start to define Vuex Authentication module that contains:

  • state: { status, user }
  • actions: { login, logout, register }
  • mutations: { loginSuccess, loginFailure, logout, registerSuccess, registerFailure }

We use AuthService which is defined above to make authentication requests.

auth.module.js

import AuthService from '../services/auth.service';

const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
  ? { status: { loggedIn: true }, user }
  : { status: {}, user: null };

export const auth = {
  namespaced: true,
  state: initialState,
  actions: {
    login({ commit }, user) {
      return AuthService.login(user).then(
        user => {
          commit('loginSuccess', user);
          return Promise.resolve(user);
        },
        error => {
          commit('loginFailure');
          return Promise.reject(error.response.data);
        }
      );
    },
    logout({ commit }) {
      AuthService.logout();
      commit('logout');
    },
    register({ commit }, user) {
      return AuthService.register(user).then(
        response => {
          commit('registerSuccess');
          return Promise.resolve(response.data);
        },
        error => {
          commit('registerFailure');
          return Promise.reject(error.response.data);
        }
      );
    }
  },
  mutations: {
    loginSuccess(state, user) {
      state.status = { loggedIn: true };
      state.user = user;
    },
    loginFailure(state) {
      state.status = {};
      state.user = null;
    },
    logout(state) {
      state.status = {};
      state.user = null;
    },
    registerSuccess(state) {
      state.status = {};
    },
    registerFailure(state) {
      state.status = {};
    }
  }
};

You can find more details about Vuex at Vuex Guide.

Create Vue Authentication Components

Define User model

To make code clear and easy to read, we define the User model first.
Under src/models folder, create user.js like this.

export default class User {
  constructor(username, email, password) {
    this.username = username;
    this.email = email;
    this.password = password;
  }
}

Let’s continue with Authentication Components.
Instead of using axios or AuthService directly, these Components should work with Vuex Store:
– getting status with this.$store.state.auth
– making request by dispatching an action: this.$store.dispatch()


views

Login.vue

Register.vue

Profile.vue


Vue Login Page

In src/views folder, create Login.vue file with following code:

<template>
  <div class="col-md-12">
    <div class="card card-container">
      <img
        id="profile-img"
        src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
        class="profile-img-card"
      />
      <form name="form" @submit.prevent="handleLogin">
        <div class="form-group">
          <label for="username">Username</label>
          <input
            type="text"
            class="form-control"
            name="username"
            v-model="user.username"
            v-validate="'required'"
          />
          <div
            class="alert alert-danger"
            role="alert"
            v-if="errors.has('username')"
          >Username is required!</div>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input
            type="password"
            class="form-control"
            name="password"
            v-model="user.password"
            v-validate="'required'"
          />
          <div
            class="alert alert-danger"
            role="alert"
            v-if="errors.has('password')"
          >Password is required!</div>
        </div>
        <div class="form-group">
          <button class="btn btn-primary btn-block" :disabled="loading">
            <span class="spinner-border spinner-border-sm" v-show="loading"></span>
            <span>Login</span>
          </button>
        </div>
        <div class="form-group">
          <div class="alert alert-danger" role="alert" v-if="message">{{message}}</div>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import User from '../models/user';

export default {
  name: 'login',
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    }
  },
  data() {
    return {
      user: new User('', ''),
      loading: false,
      message: ''
    };
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push('/profile');
    }
  },
  methods: {
    handleLogin() {
      this.loading = true;
      this.$validator.validateAll();

      if (this.errors.any()) {
        this.loading = false;
        return;
      }

      if (this.user.username && this.user.password) {
        this.$store.dispatch('auth/login', this.user).then(
          () => {
            this.$router.push('/profile');
          },
          error => {
            this.loading = false;
            this.message = error.message;
          }
        );
      }
    }
  }
};
</script>

<style scoped>
label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}
</style>

This page has a Form with username & password. We use [VeeValidate 2.x](http://<a href=) to validate input before submitting the form. If there is an invalid field, we show the error message.

We check user logged in status using Vuex Store: this.$store.state.auth.status.loggedIn. If the status is true, we use Vue Router to direct user to Profile Page:

created() {
  if (this.loggedIn) {
    this.$router.push('/profile');
  }
},

In the handleLogin() function, we dispatch 'auth/login' Action to Vuex Store. If the login is successful, go to Profile Page, otherwise, show error message.

Vue Register Page

This page is similar to Login Page.

For form validation, we have some more details:

  • username: required|min:3|max:20
  • email: required|email|max:50
  • password: required|min:6|max:40

For form submission, we dispatch 'auth/register' Vuex Action.

src/views/Register.vue

<template>
  <div class="col-md-12">
    <div class="card card-container">
      <img
        id="profile-img"
        src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
        class="profile-img-card"
      />
      <form name="form" @submit.prevent="handleRegister">
        <div v-if="!successful">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              class="form-control"
              name="username"
              v-model="user.username"
              v-validate="'required|min:3|max:20'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('username')"
            >{{errors.first('username')}}</div>
          </div>
          <div class="form-group">
            <label for="email">Email</label>
            <input
              type="email"
              class="form-control"
              name="email"
              v-model="user.email"
              v-validate="'required|email|max:50'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('email')"
            >{{errors.first('email')}}</div>
          </div>
          <div class="form-group">
            <label for="password">Password</label>
            <input
              type="password"
              class="form-control"
              name="password"
              v-model="user.password"
              v-validate="'required|min:6|max:40'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('password')"
            >{{errors.first('password')}}</div>
          </div>
          <div class="form-group">
            <button class="btn btn-primary btn-block">Sign Up</button>
          </div>
        </div>
      </form>

      <div
        class="alert"
        :class="successful ? 'alert-success' : 'alert-danger'"
        v-if="message"
      >{{message}}</div>
    </div>
  </div>
</template>

<script>
import User from '../models/user';

export default {
  name: 'register',
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    }
  },
  data() {
    return {
      user: new User('', '', ''),
      submitted: false,
      successful: false,
      message: ''
    };
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push('/profile');
    }
  },
  methods: {
    handleRegister() {
      this.message = '';
      this.submitted = true;
      this.$validator.validate().then(valid => {
        if (valid) {
          this.$store.dispatch('auth/register', this.user).then(
            data => {
              this.message = data.message;
              this.successful = true;
            },
            error => {
              this.message = error.message;
              this.successful = false;
            }
          );
        }
      });
    }
  }
};
</script>

<style scoped>
label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}
</style>

Profile Page

This page gets current User from Vuex Store and show information. If the User is not logged in, it directs to Login Page.

src/views/Profile.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>
        <strong>{{currentUser.username}}</strong> Profile
      </h3>
    </header>
    <p>
      <strong>Token:</strong>
      {{currentUser.accessToken.substring(0, 20)}} ... {{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}}
    </p>
    <p>
      <strong>Id:</strong>
      {{currentUser.id}}
    </p>
    <p>
      <strong>Email:</strong>
      {{currentUser.email}}
    </p>
    <strong>Authorities:</strong>
    <ul>
      <li v-for="(role,index) in currentUser.roles" :key="index">{{role}}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'profile',
  computed: {
    currentUser() {
      return this.$store.state.auth.user;
    }
  },
  mounted() {
    if (!this.currentUser) {
      this.$router.push('/login');
    }
  }
};
</script>

Create Vue Components for accessing Resources

These components will use UserService to request data.


views

Home.vue

BoardAdmin.vue

BoardModerator.vue

BoardUser.vue


Home Page

This is a public page.

src/views/Home.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>{{content}}</h3>
    </header>
  </div>
</template>

<script>
import UserService from '../services/user.service';

export default {
  name: 'home',
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    UserService.getPublicContent().then(
      response => {
        this.content = response.data;
      },
      error => {
        this.content = error.response.data.message;
      }
    );
  }
};
</script>

Role-based Pages

We have 3 pages for accessing protected data:

  • BoardUser page calls UserService.getUserBoard()
  • BoardModerator page calls UserService.getModeratorBoard()
  • BoardAdmin page calls UserService.getAdminBoard()

This is an example, other Page are similar to this Page.

src/views/BoardUser.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>{{content}}</h3>
    </header>
  </div>
</template>

<script>
import UserService from '../services/user.service';

export default {
  name: 'user',
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    UserService.getUserBoard().then(
      response => {
        this.content = response.data;
      },
      error => {
        this.content = error.response.data.message;
      }
    );
  }
};
</script>

Define Routes for Vue Router

Now we define all routes for our Vue Application.

src/router.js

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import Login from './views/Login.vue';
import Register from './views/Register.vue';

Vue.use(Router);

export const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/home',
      component: Home
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/register',
      component: Register
    },
    {
      path: '/profile',
      name: 'profile',
      // lazy-loaded
      component: () => import('./views/Profile.vue')
    },
    {
      path: '/admin',
      name: 'admin',
      // lazy-loaded
      component: () => import('./views/BoardAdmin.vue')
    },
    {
      path: '/mod',
      name: 'moderator',
      // lazy-loaded
      component: () => import('./views/BoardModerator.vue')
    },
    {
      path: '/user',
      name: 'user',
      // lazy-loaded
      component: () => import('./views/BoardUser.vue')
    }
  ]
});

Add Navigation Bar to Vue App

This is the root container for our application that contains navigation bar. We will add router-view here.

src/App.vue

<template>
  <div id="app">
    <nav class="navbar navbar-expand navbar-dark bg-dark">
      <a href="#" class="navbar-brand">bezKoder</a>
      <div class="navbar-nav mr-auto">
        <li class="nav-item">
          <a href="/home" class="nav-link">
            <font-awesome-icon icon="home" /> Home
          </a>
        </li>
        <li class="nav-item" v-if="showAdminBoard">
          <a href="/admin" class="nav-link">Admin Board</a>
        </li>
        <li class="nav-item" v-if="showModeratorBoard">
          <a href="/mod" class="nav-link">Moderator Board</a>
        </li>
        <li class="nav-item">
          <a href="/user" class="nav-link" v-if="currentUser">User</a>
        </li>
      </div>

      <div class="navbar-nav ml-auto" v-if="!currentUser">
        <li class="nav-item">
          <a href="/register" class="nav-link">
            <font-awesome-icon icon="user-plus" /> Sign Up
          </a>
        </li>
        <li class="nav-item">
          <a href="/login" class="nav-link">
            <font-awesome-icon icon="sign-in-alt" /> Login
          </a>
        </li>
      </div>

      <div class="navbar-nav ml-auto" v-if="currentUser">
        <li class="nav-item">
          <a href="/profile" class="nav-link">
            <font-awesome-icon icon="user" />
            {{currentUser.username}}
          </a>
        </li>
        <li class="nav-item">
          <a href class="nav-link" @click="logOut">
            <font-awesome-icon icon="sign-out-alt" /> LogOut
          </a>
        </li>
      </div>
    </nav>

    <div class="container">
      <router-view />
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    currentUser() {
      return this.$store.state.auth.user;
    },
    showAdminBoard() {
      if (this.currentUser) {
        return this.currentUser.roles.includes('ROLE_ADMIN');
      }

      return false;
    },
    showModeratorBoard() {
      if (this.currentUser) {
        return this.currentUser.roles.includes('ROLE_MODERATOR');
      }

      return false;
    }
  },
  methods: {
    logOut() {
      this.$store.dispatch('auth/logout');
      this.$router.push('/login');
    }
  }
};
</script>

Our navbar looks more professional when using font-awesome-icon.
We also make the navbar dynamically change by current User’s roles which are retrieved from Vuex Store state.

Handle Unauthorized Access

If you want to check Authorized status everytime a navigating action is trigger, just add router.beforeEach() at the end of src/router.js like this:

router.beforeEach((to, from, next) => {
  const publicPages = ['/login', '/home'];
  const authRequired = !publicPages.includes(to.path);
  const loggedIn = localStorage.getItem('user');

  // try to access a restricted page + not logged in
  if (authRequired && !loggedIn) {
    return next('/login');
  }

  next();
});

Conclusion

Congratulation!

Today we’ve done so many interesting things. I hope you understand the overall layers of our Vue application, and apply it in your project at ease. Now you can build a front-end app that supports JWT Authentication with Vue.js, Vuex and Vue Router.

Happy learning, see you again!

Unit testing in NativeScript Vue

Unit testing in NativeScript Vue

While the experience of working on mobile apps using NativeScript Vue has been for the most part, one of the roadblocks I quickly hit was unit testing. The Vue way of Jest + Vue Test Utils does not work out of the box with NativeScript Vue. Vanilla NativeScript supports unit testing via Karma and a choice of Jasmine,

While the experience of working on mobile apps using NativeScript Vue has been for the most part, one of the roadblocks I quickly hit was unit testing. The Vue way of Jest + Vue Test Utils does not work out of the box with NativeScript Vue. Vanilla NativeScript supports unit testing via Karma and a choice of Jasmine, Mocha etc but they do not work well with NativeScript Vue as well. Without any official documentation from NativeScript Vue on unit testing, I’m documenting down my way of unit testing NativeScript Vue with Jest and Vue Test Utils.

Initial setup

Before we continue, please be warned that this guide was done based on the JavaScript flavor of NativeScript Vue. Your mileage may vary on TypeScript flavored projects. The first few steps are largely similar with how you would set up unit testing in vanilla Vue projects.

Let’s start by installing Jest. Jest is the test runner we will use to execute our unit tests.

npm i jest -D

Next, we install Vue Test Utils and other dependencies. Being able to work with Vue Test Utils is a huge plus because it has a ton of useful helpers that make working with Vue’s Single File Components painless.

npm i @vue/test-utils vue-jest babel-jest -D

We define a test script in our package.json.

{
  "scripts": {
    "test": "jest"
  }
}

We also define our configuration for Jest. We have a little extra in the moduleNameMapper section. This is because NativeScript Vue already has a couple of aliases defined in the Webpack config file. We want to replicate these aliases into Jest or your tests will fail if you are using them in your imports.

module.exports = {
  verbose: true,
  moduleFileExtensions: [
    'js',
    'json',
    'vue',
  ],
  moduleDirectories: [
    'node_modules',
  ],
  moduleNameMapper: {
    // Default NativeScript webpack aliases
    '~/(.*)$': '<rootDir>/app/$1',
    '@/(.*)$': '<rootDir>/app/$1',
    '^projectRoot/(.*)$': '<rootDir>/$1',
  },
  transform: {
    '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
    '.*\\.(vue)$': 'vue-jest',
  },
  transformIgnorePatterns: [],
  collectCoverage: true,
  collectCoverageFrom: [
    'app/**/*.{js,vue}',
  ],
  coverageReporters: ['text-summary', 'html'],
};

With jest configured, we need to configure Babel for our test environment too. As of October 2019, the standard practice is to use @babel/preset-env. Let’s get that installed, and then open up our Babel config babel.config.js.

npm i -D [email protected] @babel/preset-env
// babel.config.js
module.exports = function (api) {
  api.cache(true)
  return {
    presets: [
      ['@babel/preset-env', { targets: { esmodules: true } }],
    ],
    env: {
      test: {
        presets: [['@babel/preset-env', {
          targets: { node: 'current' },
        }]],
      },
    },
  }
}

With the initial set up of Jest, Babel, and Vue Test Utils, let’s try writing our first basic unit test. In this guide, we’re basing it off nativescript-vue/vue-cli-template so there is already an App.vue component. Let’s create a spec file for App.vue in the same folder as the component.

// App.spec.js
import { shallowMount } from '@vue/test-utils';
import App from './App';
describe('App.vue', () => {
  it('mounts and renders', () => {
    const wrapper = shallowMount(App);
    expect(wrapper.html()).toBeTruthy();
  });
});

Try running the test now and you will quickly see it doesn’t work, yet.

Cannot find module 'vue' from 'vue-test-utils.js'
NativeScript Vue specific setup

We’re getting the error above because the Vue we are using in our project is nativescript-vue. vue itself is not installed in our project, hence Vue Test Utils is unable to resolve the module vue. NativeScript Vue components are also for the most part similar to standard Vue components which also means we can use standard Vue.js in our unit tests which will work much better with Jest. We’ll install standard Vue.js and the standard vue-template-compiler to be able to compile our single file components.

npm i vue vue-template-compiler -D

Run your tests again and this time you’ll see that they pass! Your App.vue was mounted and the tests executed sucessfully against it. However, you might notice a couple of warnings being output in the console now:

[Vue warn]: Unknown custom element: <Page> - did you register the component correctly? For recur
sive components, make sure to provide the "name" option.

[Vue warn]: Unknown custom element: <ActionBar> - did you register the component correctly? For recursive components, make sure to provide the "name" option.

NativeScript Vue introduces a fair amount of new components that are registered globally. These aren’t available in our test environment, so let’s stub them. Back in our jest.config.js file, let’s add an entry for a global setup file to be executed before tests. We will define our stubs in this setup file.

// jest.config.js
module.exports = {
  setupFiles: [
    '<rootDir>/jest/nativescript-vue-stubs.js',
  ]
}

Create the setup file, which we’ve defined to be in the jest directory, which we also would need to create. We’ll add components that we need stubbed out into the Vue Test Utils config.

// jest/nativescript-vue-stubs.js
import { config } from '@vue/test-utils';

const NSElements = [
  'ActionBar',
  'ActionItem',
  'FormattedString',
  'GridLayout',
  'HtmlView',
  'NavigationButton',
  'Page',
  'StackLayout',
  'TabView',
  'TabViewItem',
  'TextField',
];

NSElements.forEach((ele) => {
  config.stubs[ele] = true;
});

Should you come across any other console errors related to NativeScript Vue components, you can add them to the NSElements array in this file to stub them out.

Working with TNS Core Modules

Your project will most likely also be accessing the native runtimes and have platform-specific code. For example, one common pattern would be checking whether the current platform is Android or iOS. Taking App.vue as an example, your Vue component might look something like this:

// App.vue
<template>
  <Page>
    <ActionBar title="Welcome to NativeScript-Vue!"/>
    <GridLayout columns="*" rows="*">
      <Label class="message" :text="msg" col="0" row="0"/>

      <Label v-if="isIOS" text="This text is only visible on iOS" />
    </GridLayout>
  </Page>
</template>

<script >
  import { isIOS } from 'tns-core-modules/platform';

  export default {
    data() {
      return {
        msg: 'Hello World!',
        isIOS,
      }
    }
  }
</script>

The native runtimes of NativeScript Vue have their individual platform specific file extensions of .ios.js and .android.js. We’ll need to configure Jest to find the tns-core-modules and to recognize the native runtime JS files as modules. Add the native runtime file extensions in your jest.config.js file:

// jest.config.js
module.exports = {
  moduleFileExtensions: [
    'android.js',
    'ios.js',
    'js',
    'json',
    'vue',
  ],
}
NativeScript plugins: __extends is not defined

If you use other NativeScript plugins, you may also get this error __extends is not defined. This is because some plugins were written in TypeScript and did not have the TypeScript helpers included during compilation. This is easy to get around with by defining them globally before your tests run. I have included a couple of the more common ones that I required in my project. Should you need more, the TSLib Github repo is a good resource to adapt your own.

Create another file in your Jest folder: jest/typescript-helpers.js:

global.__extends = (this && this.__extends) || function (d, b) {
  for (let p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
  function __() { this.constructor = d; }
  d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};

global.__assign = (this && this.__assign) || Object.assign || function (t) {
  for (var s, i = 1, n = arguments.length; i < n; i++) {
    s = arguments[i];
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
  }
  return t;
};

global.__rest = (this && this.__rest) || function (s, e) {
  var t = {};
  for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
    t[p] = s[p];
  if (s != null && typeof Object.getOwnPropertySymbols === "function") {
    for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
      if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) {
        t[p[i]] = s[p[i]];
      }
    }
  }
  return t;
}

global.__decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  return c > 3 && r && Object.defineProperty(target, key, r), r;
};

Make sure you include this in your Jest config’s setupFiles:

setupFiles: [
  '<rootDir>/jest/typescript-helpers.js',
  '<rootDir>/jest/nativescript-vue-stubs.js',
],
Triggering events on NativeScript components

Remember that we have been stubbing out all the NativeScript Vue components. One common pattern would be to trigger a tap on a component to test the function executed. Since they are stubs, you will need to emit a tap event instead of triggering it:

// This will not work
wrapper.find('actionitem-stub').trigger('tap');

// Instead, emit an from the stub's ViewModel
wrapper.find('actionitem-stub').vm.emit('ta');
Happy Testing

With all these done, you should be able to enjoy a smooth development experience writing unit tests for your NativeScript Vue app. I have also created an example repo of a very basic setup of a NativeScript Vue app with Jest and Vue Test Utils configured. You are free to use it however you’d like, even as a base for your next project. I hope you’ve found this guide useful.