Categories
Vue Testing

Unit Test Vue Apps with Vue Test Utils

With the Vue Test Utils library, we can write and run unit tests for Vue apps easily.

In this article, we’ll look at how to write unit tests with the Vue Test Utils library.

Installation

We can add it when we run Vue CLI to scaffold our app.

We select Unit Tests from the list and then select Jest to add the test framework with the production code.

Usage

Once we added the test files, we can write our unit tests.

In our unit tests, we can interact with our components like we do when using them.

To do that, we mount the component and interact with the elements by triggering events on them.

Also, we can mock any data and external resources that need to be set to let us do the tests.

This way, we can run our tests independently without relying on anything external that makes the tests unreliable.

To write our first test, we can add the test in the tests/unit folder by writing:

import { shallowMount } from '@vue/test-utils'

const MessageComponent = {
  template: '<p>{{ msg }}</p>',
  props: ['msg']
}

describe('MessageComponent', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(MessageComponent, {
      propsData: { msg }
    })
    expect(wrapper.text()).toMatch(msg)
  })
})

We have the MessageComponent that renders the value of the msg prop onto the screen.

In our test, which is in the callback we pass into it , we call shallowMount to mount the component without its children.

propsData lets us pass the prop data we need for the test.

Then the wrapper.text() method returns the rendered text.

And we use toMatch to check the results.

To run the test, we run npm run test:unit to run the unit test and then we should see some green text to indicate that the test passed.

Simulating User Interaction

We can also simulate user interaction easily.

To do that, we can write:

import { shallowMount } from '@vue/test-utils'

const Counter = {
  template: `
    <div>
      <button @click="count++">Add up</button>
      <p>clicks: {{ count }}</p>
    </div>
  `,
  data() {
    return { count: 0 }
  }
}

describe('Counter', () => {
  it('renders new number when Add up button is clicked', async () => {
    const wrapper = shallowMount(Counter)
    const button = wrapper.find('button')
    const text = wrapper.find('p')
    await button.trigger('click')
    expect(text.text()).toBe('clicks: 1')
  })
})

We have the Counter component that we want to test.

It has a button that increases count by 1 when we click it.

Then in the test, we call shallowMount to mount the component.

Then we call wrapper.find to get the button and the p element.

To simulate a click on the button, we call trigger to trigger the click event.

trigger is async is returns a promise, so our test callback has to be an async function and we have to await the trigger method.

Then we check the text displayed in the p element with the text method and see if the text has been updated.

Conclusion

We can write unit tests easily to test Vue apps by using Jest and simulating user actions.

Categories
Vue Testing

Unit Test Vue Apps with Vue Test Utils — Child Components and Reactive Properties

With the Vue Test Utils library, we can write and run unit tests for Vue apps easily.

In this article, we’ll look at how to write unit tests with the Vue Test Utils library.

Emitting Event from Child Component

We can check for events that are emitted in a child component.

To do that, we call mount to mount the parent component with the child component.

Then we can emit the event we want on the child:

import { mount } from '@vue/test-utils'

const ChildComponent = {
  template: `
    <div>
      <p>child</p>
    </div>
  `,
}

const ParentComponent = {
  template: `
    <div>
      <child-component @custom="onCustom" />
      <p id='emitted' v-if="emitted">emitted</p>
    </div>
  `,
  data() {
    return { emitted: false }
  },
  methods: {
    onCustom() {
      this.emitted = true
    }
  },
  components: {
    ChildComponent
  }
}

describe('ParentComponent', () => {
  it('shows emitted when child-component emits custom event', async () => {
    const wrapper = mount(ParentComponent)
    await wrapper.findComponent(ChildComponent).vm.$emit('custom')
    expect(wrapper.find('#emitted').text()).toContain('emitted')
  })
})

To mount the child component of ParentComponent with the ParentComponent , we call the mount method.

The findComponent method has ChildComponent as the argument so that we can use the returned component wrapper to emit the custom event with the $emit method.

Then to check if the ‘emitted’ text is displayed, we call the find method to check if it has the 'emitted' string.

Manipulating Component State

We can manipulate component state with the setData or setProps method.

For example, we can write:

import { mount } from '@vue/test-utils'

const Counter = {
  template: `
    <div>
      <p>{{count}}</p>
    </div>
  `,
  data() {
    return { count: 0 }
  },
}

describe('Counter', () => {
  it('renders 10 for count', async () => {
    const wrapper = mount(Counter)
    await wrapper.setData({ count: 10 })
    expect(wrapper.find('p').text()).toContain(10)
  })
})

to call the setData method with the data we want to set.

The key is the key of a reactive property and the value is its value.

To set props, we can use the setProps method:

import { mount } from '@vue/test-utils'

const Foo = {
  template: `
    <div>
      <p>{{foo}}</p>
    </div>
  `,
  props: ['foo']
}

describe('Foo', () => {
  it('renders bar for foo prop', async () => {
    const wrapper = mount(Foo)
    await wrapper.setProps({ foo: 'bar' })
    expect(wrapper.find('p').text()).toContain('bar')
  })
})

We call the setProps method to set the value of the foo prop to 'bar' .

Then we check that if that is rendered in the last line of the test.

Props can also be set when we mount the component:

import { mount } from '@vue/test-utils'

const Foo = {
  template: `
    <div>
      <p>{{foo}}</p>
    </div>
  `,
  props: ['foo']
}

describe('Foo', () => {
  it('renders bar for foo prop', async () => {
    const wrapper = mount(Foo, {
      propsData: {
        foo: 'bar'
      }
    })
    expect(wrapper.find('p').text()).toContain('bar')
  })
})

The 2nd argument of the mount method takes the propsData property to let us mock the props.

Conclusion

We can mock props and set values of reactive properties in our tests and test them.

Child component events can also be tested from the parent.

Categories
Vue

How to Use Axios with Vuex

Vuex is a state management library for Vue apps.

It’s the most popular choice for state management for Vue apps.

In this article, we’ll look at how to use the Axios HTTP client with Vuex.

Run Async Code with Actions

We can run async code in a Vuex store within actions.

To add an action, we just add the action property into the object we pass into the Vuex.Store constructor.

For example, we can write:

main.js

import Vue from "vue";
import App from "./App.vue";
import Vuex from "vuex";

Vue.config.productionTip = false;
Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++;
    }
  },
  actions: {
    increment(context) {
      context.commit("increment");
    }
  }
});

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

to add the actions property with the increment method.

The context.commit method commits the mutation we want to change the state.

Then we can dispatch the action in our component with the this.$store.dispatch method:

App.vue

<template>
  <div id="app">
    <button @click="increment">increment</button>
    <p>{{count}}</p>
  </div>
</template>

<script>
export default {
  name: "App",
  computed: {
    count() {
      return this.$store.state.count;
    }
  },
  methods: {
    increment() {
      this.$store.dispatch("increment");
    }
  }
};
</script>

Since actions can have async code in it, we can just add the async code we want in the action method.

The code can update the state.

To use Axios to make a request in our action, we write:

main.js

import Vue from "vue";
import App from "./App.vue";
import Vuex from "vuex";
import axios from "axios";

Vue.config.productionTip = false;
Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    post: {}
  },
  mutations: {
    setPost(state, data) {
      state.post = data;
    }
  },
  actions: {
    async getPost(context, { id }) {
      const { data } = await axios.get(
        `https://jsonplaceholder.typicode.com/posts/${id}`
      );
      context.commit("setPost", data);
    }
  }
});

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

App.vue

<template>
  <div id="app">
    <button @click="getPost">get post</button>
    <p>{{post}}</p>
  </div>
</template>

<script>
export default {
  name: "App",
  computed: {
    post() {
      return this.$store.state.post;
    }
  },
  methods: {
    getPost() {
      this.$store.dispatch("getPost", { id: 1 });
    }
  }
};
</script>

The difference is that we have the getPost async method to make the request to the API.

Then once we have the result, we call context.commit to commit the setPost mutation to update the state.

In the setPost mutation method, we get the data parameter’s value and then set it as the value of state.post to update the post state.

In App.vue , we dispatch the getPost action with the this.$store.dispatch method the same way.

Just that we add an object with the id property to pass it to the getPost action method as the 2nd parameter.

In the post computed property, we return the value of the this.$store.state.post property.

Now when we click the get post button, we should see the post data displayed from the post state via the post computed property.

Conclusion

We can use Axios with Vuex as long as we put the Axios code in the Vuex store action.

An action method can have async code but mutations can’t.

Categories
Vue

Build a Multi-Step Form in a Vue App with the vue-stepper Package

To build multi-step forms easily in a Vue app, we can use the vue-stepper package to help us.

In this article, we’ll look at how to use the vue-stepper package to add a multi-step form into our app.

Installation

To install the library, we run:

npm i vue-stepper

Create a Multistep Form

To create a multistep form, we need a component for the container.

Also, we need a component to display the step content.

For example, we can write:

App.vue

<template>
  <div id="app">
    <horizontal-stepper
      :steps="demoSteps"
      @completed-step="completeStep"
      @active-step="isStepActive"
      @stepper-finished="alert"
    ></horizontal-stepper>
  </div>
</template>

<script>
import HorizontalStepper from "vue-stepper";
import StepOne from "./components/StepOne";
import StepTwo from "./components/StepTwo";

export default {
  name: "App",
  components: {
    HorizontalStepper
  },
  data() {
    return {
      demoSteps: [
        {
          icon: "mail",
          name: "first",
          title: "Step 1",
          subtitle: "Step 1",
          component: StepOne,
          completed: false
        },
        {
          icon: "report_problem",
          name: "second",
          title: "Step 2",
          subtitle: "Step 2",
          component: StepTwo,
          completed: false
        }
      ]
    };
  },
  methods: {
    completeStep(payload) {
      this.demoSteps.forEach(step => {
        if (step.name === payload.name) {
          step.completed = true;
        }
      });
    },
    isStepActive(payload) {
      this.demoSteps.forEach(step => {
        if (step.name === payload.name) {
          if (step.completed === true) {
            step.completed = false;
          }
        }
      });
    },
    alert(payload) {
      alert("end");
    }
  }
};
</script>

StepOne.vue

<template>
  <div>
    <input placeholder="email" v-model="email" @input="onChange">
  </div>
</template>

<script>
export default {
  data() {
    return {
      email: ""
    };
  },
  beforeMount(){
    if (this.email){
      this.$emit("can-continue", { value: true });
    }
  },
  methods: {
    onChange() {
      this.$emit("can-continue", { value: true });
    }
  }
};
</script>

StepTwo.vue

<template>
  <div>
    <input placeholder="problem" v-model="problem" @input="onChange">
  </div>
</template>

<script>
export default {
  data() {
    return {
      problem: ""
    };
  },
    beforeMount(){
    if (this.problem){
      this.$emit("can-continue", { value: true });
    }
  },
  methods: {
    onChange() {
      this.$emit("can-continue", { value: true });
    }
  }
};
</script>

In App.vue , we have the container for the steps.

The template has the horizontal-stepper component that has the icon, title, and subtitle of the form.

The completed-step event is emitted when we click next.

active-step is emitted when we go to a different step.

stepper-finished is emitted when all the steps are finished.

StepOne.vue and StepTwo.vue are our step components.

We set them as the content with the component property in the demoSteps array.

When we type some into the inputs, then the input event is emitted.

The onChange method is run when the input method is emitted so the can-continue event is emitted. When it’s emitted with an object with the value property set to true , then the Next button is enabled.

Now we can continue with the other steps.

Once we click the Finish button, then the stepper-finished event is emitted and we can see an alert because the alert method is run.

Conclusion

We can add a multi-step form into our Vue app with the vue-stepper package.

Categories
Vue

Make HTTP Requests in a Vue App with Fetch

The Fetch API is an HTTP client that is built into modern browsers.

Therefore, we can use it to make HTTP requests without installing anything.

In this article, we’ll look at how to make requests with the Fetch API in a Vue app.

Making Requests

We can make requests with the fetch function.

For example, we can write:

<template>
  <div id="app"></div>
</template>
<script>
export default {
  name: "App",
  async beforeMount() {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts/1", {
      method: "GET",
      mode: "cors"
    });
    const data = await res.json();
    console.log(data);
  }
};
</script>

to make a simple GET request with fetch .

The first argument is the URL.

And the 2nd is an object with the method and mode properties.

The method property is the request method.

The mode is set to 'cors' to let us make cross-origin requests.

To make a POST request, we can rite:

<template>
  <div id="app"></div>
</template>
<script>
export default {
  name: "App",
  async beforeMount() {
    const res = await fetch("https://jsonplaceholder.typicode.com/posts", {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        title: "foo",
        body: "bar",
        userId: 1
      })
    });
    const data = await res.json();
    console.log(data);
  }
};
</script>

We set the headers property to set the request headers.

The body property has the stringified JSON object used for the request body.

Uploading a File

To upload a file, we can write:

<template>
  <div id="app">
    <input type="file" @change="onChange">
  </div>
</template>
<script>
export default {
  name: "App",
  methods: {
    async onChange(ev) {
      const formData = new FormData();
      formData.append("username", "abc123");
      formData.append("avatar", ev.target.files[0]);
      const res = await fetch(
        "https://run.mocky.io/v3/c5189845-2a93-49aa-85c7-70bc64e8af90 ",
        {
          method: "PUT",
          body: formData
        }
      );
      console.log(res);
    }
  }
};
</script>

We have a file input and a change event listener to listen for file selection changes.

The onChange method is the change listener.

And in it, we create a FormData instance and add the file to it with the ev.target.files[0] file object.

Then we make the request by setting the body property to the formData object.

For example, we can write:

<template>
  <div id="app">
    <input type="file" @change="onChange" multiple>
  </div>
</template>
<script>
export default {
  name: "App",
  methods: {
    async onChange(ev) {
      const formData = new FormData();
      formData.append("username", "abc123");
      for (const file of ev.target.files) {
        formData.append("photos", file);
      }
      const res = await fetch(
        "https://run.mocky.io/v3/c5189845-2a93-49aa-85c7-70bc64e8af90 ",
        {
          method: "PUT",
          body: formData
        }
      );
      console.log(res);
    }
  }
};
</script>

We loop through the ev.target.files object with the for-of loop to add all the files from the object to the request body.

Check if a Request is Successful

We can check if a request is successful by catching the error.

Only network errors are raised. This doesn’t include completed requests with 400 or 500 series error codes.

For example, we can write:

<template>
  <div id="app"></div>
</template>
<script>
export default {
  name: "App",
  async beforeMount() {
    try {
      const res = await fetch("https://picsum.photos/200");
      const data = await res.blob();
      console.log(data);
    } catch (error) {
      console.error(error);
    }
  }
};
</script>

to catch the error with the catch block.

Conclusion

We can make HTTP requests in a Vue app with the Fetch API.