Categories
Vue 3 Testing

Testing Vue 3 Apps — Testing with a Mocked Router

With apps getting more complex than ever, it’s important to test them automatically. We can do this with unit tests, and then we don’t have to test everything by hand.

In this article, we’ll look at how to test Vue 3 apps by writing a simple app and testing it.

Test Apps with Vue Router

We can test apps that use the Vue Router with a mock router.

This way, we can test our component in isolation without worrying about the router.

src/views/Edit.vue

<template>
  <button @click="redirect">Click to Edit</button>
</template>

<script>
export default {
  props: ["authenticated"],
  methods: {
    redirect() {
      if (this.authenticated) {
        this.$router.push(`/posts/${this.$route.params.id}/edit`);
      } else {
        this.$router.push("/404");
      }
    },
  },
};
</script>

src/views/Edit.vue

<template>
  <div>Post {{$route.params.id}}</div>
</template>

src/views/NotFound.vue

<template>
  <div>404</div>
</template>

index.js

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Edit from '../views/Edit.vue'
import Post from '../views/Post.vue'
import NotFound from '../views/NotFound.vue'

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/edit/:id',
    name: 'Edit',
    component: Edit
  },
  {
    path: '/posts/:id',
    name: 'post',
    component: Post
  },
  {
    path: '/404',
    name: 'not-found',
    component: NotFound
  },
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

export default router

tests/unit/example.spec.js

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

describe('component handles routing correctly', () => {
  it('allows authenticated user to edit a post', async () => {
    const mockRoute = {
      params: {
        id: 1
      }
    }
    const mockRouter = {
      push: jest.fn()
    }
    const wrapper = mount(Edit, {
      props: {
        authenticated: true
      },
      global: {
        mocks: {
          $route: mockRoute,
          $router: mockRouter
        }
      }
    })
    await wrapper.find('button').trigger('click')
    expect(mockRouter.push).toHaveBeenCalledWith('/posts/1/edit')
  })

  it('redirect an unauthenticated user to 404', async () => {
    const mockRoute = {
      params: {
        id: 1
      }
    }
    const mockRouter = {
      push: jest.fn()
    }
    const wrapper = mount(Edit, {
      props: {
        authenticated: false
      },
      global: {
        mocks: {
          $route: mockRoute,
          $router: mockRouter
        }
      }
    })
    await wrapper.find('button').trigger('click')
    expect(mockRouter.push).toHaveBeenCalledWith('/404')
  })
})

The Vue app has several routes and we map them to routes.

Also, we added a group of tests that uses the mock router to test the components.

We add the mockRoute to add a mock route object.

And we create the mockRouter by adding the push method to it.

Its value is a mocked function.

Also, when we mount the component, we pass in the props and global properties.

The mocks have the $route and $router properties that we want to mock.

In the first test, we get the button in the Edit component and click it.

And we check that the mockRouter.push method goes to /posts/1/edit .

In the 2nd test, we do the same thing but with the the authenticated prop set to false .

And we check that we’re redirected to the /404 route when we click on the button.

Conclusion

We can test our components with a mocked version of Vue Router in our Vue 3 app tests.

Categories
Vue 3 Testing

Testing Vue 3 Apps — Apps with Vuex

With apps getting more complex than ever, it’s important to test them automatically. We can do this with unit tests, and then we don’t have to test everything by hand.

In this article, we’ll look at how to test Vue 3 apps by writing a simple app and testing it.

Testing Vuex

We can test apps with a Vuex store.

For example, if we have the following app:

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

const store = createStore({
  state() {
    return {
      count: 0
    }
  },
  mutations: {
    increment(state) {
      state.count += 1
    }
  }
})

const App = {
  template: `
    <div>
      <button @click="increment" />
      Count: {{ count }}
    </div>
  `,
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}

const app = createApp(App)
app.use(store)

We can test it with the real Vuex store.

For example, we can add the test by writing:

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

const store = createStore({
  state() {
    return {
      count: 0
    }
  },
  mutations: {
    increment(state) {
      state.count += 1
    }
  }
})

const App = {
  template: `
    <div>
      <button @click="increment" />
      Count: {{ count }}
    </div>
  `,
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}

const app = createApp(App)
app.use(store)

test('vuex', async () => {
  const wrapper = mount(App, {
    global: {
      plugins: [store]
    }
  })
  await wrapper.find('button').trigger('click')
  expect(wrapper.html()).toContain('Count: 1')
})

We pass in our store into the array we set for the plugins property.

Then when we click on the button in App , the real store’s state is updated.

Testing with a Mock Store

We can also test the app with a mock store.

For example, we can write:

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

const store = createStore({
  state() {
    return {
      count: 0
    }
  },
  mutations: {
    increment(state) {
      state.count += 1
    }
  }
})

const App = {
  template: `
    <div>
      <button @click="increment" />
      Count: {{ count }}
    </div>
  `,
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    }
  }
}

const app = createApp(App)
app.use(store)

test('vuex using a mock store', async () => {
  const $store = {
    state: {
      count: 25
    },
    commit: jest.fn()
  }
  const wrapper = mount(App, {
    global: {
      mocks: {
        $store
      }
    }
  })
  expect(wrapper.html()).toContain('Count: 25')
  await wrapper.find('button').trigger('click')
  expect($store.commit).toHaveBeenCalled()
})

We create our $store object with the commit method being a mocked function.

Then we get the rendered state.

And we click the button and check that $store.commit is called.

This is convenient since we don’t have any external dependencies in our mounted component.

However, if we break the Vuex store, then we don’t have any warnings.

Conclusion

We can test components that are connected to a Vuex store with Vue 3’s Vue Test Utils.

Categories
Vue 3 Testing

Testing Vue 3 Apps — Mock HTTP Requests

With apps getting more complex than ever, it’s important to test them automatically. We can do this with unit tests, and then we don’t have to test everything by hand.

In this article, we’ll look at how to test Vue 3 apps by writing a simple app and testing it.

Resolving Other Asynchronous Behavior

If we have async behavior in our components that are external to the component, then we a mock them.

For example, we can write:

Foo.vue

<template>
  <div>
    <p>{{ answer }}</p>
  </div>
</template>

<script>
import axios from "axios";

export default {
  data() {
    return {
      answer: "",
    };
  },
  beforeMount() {
    this.load();
  },
  methods: {
    async load() {
      const {
        data: { answer },
      } = await axios.get("https://yesno.wtf/api");
      this.answer = answer;
    },
  },
};
</script>

example.spec.js

import { mount } from 'vue-test-utils'
import flushPromises from 'flush-promises'
import axios from 'axios'
import Foo from './Foo'

jest.mock('axios', () => ({
  get: jest.fn(() => Promise.resolve({ data: { answer: 'yes' } }))
}))

test('uses a mocked axios HTTP client and flush-promises', async () => {
  const wrapper = mount(Foo)
  expect(axios.get).toHaveBeenCalledWith('https://yesno.wtf/api')
  await flushPromises()
  const div = wrapper.find('div')
  expect(div.text()).toContain('yes')
});

We have the Foo component that gets some data from the API and render it.

Then in example.spec.js , we mock the axios.get method with the jest.fn method.

It lets us spy on whether the function is called and what it’s called with.

Also, we return the data from the function.

Then in the test, we mount the component.

And then we call expect to check that axios.get is called with ‘https://yesno.wtf/api' like we did in Foo .

Then we call flushPromises to wait for the DOM to update.

And finally, we get the div and check the text with the text method.

Asserting Loading State

We can check the loading state also with our tests.

For example, we can write:

Foo.vue

<template>
  <div>
    <p v-if="loading" role="alert">Loading...</p>
    <p v-else>{{ answer }}</p>
  </div>
</template>

<script>
import axios from "axios";

export default {
  data() {
    return {
      loading: false,
      answer: "",
    };
  },
  beforeMount() {
    this.load();
  },
  methods: {
    async load() {
      this.loading = true;
      const {
        data: { answer },
      } = await axios.get("https://yesno.wtf/api");
      this.answer = answer;
      this.loading = false;
    },
  },
};
</script>

example.spec.js

import { mount } from '@vue/test-utils'
import flushPromises from 'flush-promises'
import axios from 'axios'
import Foo from './Foo'

jest.mock('axios', () => ({
  get: jest.fn(() => Promise.resolve({ data: { answer: 'yes' } }))
}))

test('uses a mocked axios HTTP client and flush-promises', async () => {
  const wrapper = mount(Foo)
  expect(wrapper.find('[role="alert"]').exists()).toBe(true)
  expect(wrapper.find('[role="alert"]').text()).toBe('Loading...')
  expect(axios.get).toHaveBeenCalledWith('https://yesno.wtf/api')
  await flushPromises()
  const div = wrapper.find('div')
  expect(div.text()).toContain('yes')
  expect(wrapper.find('[role="alert"]').exists()).toBe(false)
});

We have the Foo component that has the loading message.

It’s controlled by the loading reactive property.

Then in the test, we check if the element with the role set to alert is rendered.

And then we call flushPromises and check that the element with the role set to alert is no longer rendered.

We also check that we get the response rendered as we did with the previous example.

Conclusion

We check mock HTTP requests to test our component with Vue 3 components and Vue Test Utils.

Categories
Vue 3 Testing

Testing Vue 3 Apps — Slots and Async Behavior

With apps getting more complex than ever, it’s important to test them automatically. We can do this with unit tests, and then we don’t have to test everything by hand.

In this article, we’ll look at how to test Vue 3 apps by writing a simple app and testing it.

Testing Slots with Render Functions

We can test slots with render functions and single-file components.

For example, we can write:

Header.vue

<template>
  <div>Header</div>
</template>

example.spec.js

import { mount } from 'vue-test-utils'
import { h } from 'vue'
import Header from './Header.vue'

const Layout = {
  template: `
    <div>
      <header>
        <slot name="header" />
      </header>
      <main>
        <slot name="main" />
      </main>
      <footer>
        <slot name="footer" />
      </footer>
    </div>
  `
}

test('layout full page layout', () => {
  const wrapper = mount(Layout, {
    slots: {
      header: Header,
      main: h('div', 'Main Content'),
      footer: '<div>Footer</div>'
    }
  })

  expect(wrapper.html()).toContain('<div>Header</div>')
  expect(wrapper.html()).toContain('<div>Main Content</div>')
  expect(wrapper.html()).toContain('<div>Footer</div>')
})

We have the Layout component with several slots.

And we add a test to test it by population slots with a single file component for the header.

The main slot is populated with a render function.

h is a function to render a component. The first arg is the tag name and the 2nd arg is the content of the div.

The footer has an HTML string as its value.

Then we check its content with the expect calls.

Scoped Slots

We can test scoped slots with Vue Test Utils.

For example, we can write:

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

const ComponentWithSlots = {
  template: `
    <div class="scoped">
      <slot name="scoped" v-bind="{ msg }" />
    </div>
  `,
  data() {
    return {
      msg: 'world'
    }
  }
}

test('scoped slots', () => {
  const wrapper = mount(ComponentWithSlots, {
    slots: {
      scoped: `<template #scoped="params">
        Hello {{ params.msg }}
        </template>
      `
    }
  })
  expect(wrapper.html()).toContain('Hello world')
})

Our ComponentWithSlots component has a slot name scoped .

It exposes the msg property to the parent.

In the test, we render it in the template tag.

And we check the rendered content in the last line of the test.

Asynchronous Behavior

We can test async behavior in our tests.

For example, we can write:

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

const Counter = {
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <button @click="handleClick">Increment</button>
    </div>
  `,
  data() {
    return {
      count: 0
    }
  },
  methods: {
    handleClick() {
      this.count += 1
    }
  }
}

test('increments by 1', async () => {
  const wrapper = mount(Counter)
  await wrapper.find('button').trigger('click')
  expect(wrapper.find('p').text()).toMatch('Count: 1')
})

We mount the Counter component.

Then we get the button and trigger the click event on it.

Then we check the text of the p element to see if it’s what we expect.

Equivalently, we can write:

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

const Counter = {
  template: `
    <div>
      <p>Count: {{ count }}</p>
      <button @click="handleClick">Increment</button>
    </div>
  `,
  data() {
    return {
      count: 0
    }
  },
  methods: {
    handleClick() {
      this.count += 1
    }
  }
}

test('increments by 1', async () => {
  const wrapper = mount(Counter)
  wrapper.find('button').trigger('click')
  await nextTick()
  expect(wrapper.find('p').text()).toMatch('Count: 1')
})

We trigger the click event on the button the same way.

But we call nextTick to wait for the latest count to be rendered.

Then we can do the check the same way.

Conclusion

We can test named and scoped slots in Vue 3 components.

Also, we can test async behavior like clicks triggered in our components.

Categories
Vue 3 Testing

Testing Vue 3 Apps — Custom Inputs and Slots

With apps getting more complex than ever, it’s important to test them automatically. We can do this with unit tests, and then we don’t have to test everything by hand.

In this article, we’ll look at how to test Vue 3 apps by writing a simple app and testing it.

Test Multiple Modifiers

We can test multiple event modifiers applied to events.

For example, we can write:

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

const FormComponent = {
  template: `
    <div>
      <input @keydown.meta.c.exact.prevent="captureCopy" v-model="input" />
    </div>
  `,
  data() {
    return {
      input: ''
    }
  },
  methods: {
    captureCopy() {
      this.$emit('submit', this.input)
    }
  }
}

test('handles complex events', async () => {
  const wrapper = mount(FormComponent)
  await wrapper.find('input').trigger('keydown.meta.c.exact.prevent')
  expect(wrapper.emitted()).toHaveProperty('submit')
})

We have the FormComponent with an input that has the keydown event listener attached to it.

It has various modifiers applied to it.

Then in our test, we call trigger the keydown event with all the modifiers:

await wrapper.find('input').trigger('keydown.meta.c.exact.prevent')

Then we check if the submit event is emitted in the last line.

Vue Test Utils reads the event and applies the appropriate properties to the event object.

Interacting with Vue Component Inputs

We can interact with component inputs.

For example, we can write:

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

const CustomInput = {
  template: `
    <div>
      <label>
        {{ label }}
        <input
          type="text"
          :value="modelValue"
          [@input](https://medium.com/r/?url=http%3A%2F%2Ftwitter.com%2Finput "Twitter profile for @input")="$emit('update:modelValue', $event.target.value)"
        >
      </label>
    </div>
  `,
  name: 'CustomInput',
  props: ['modelValue', 'label']
}

const Form = {
  template: `
    <div>
      <custom-input v-model="input" label="Text Input" class="text-input"/>
    </div>
  `,
  components: {
    CustomInput
  }
}

test('fills in the form', async () => {
  const wrapper = mount(Form)
  await wrapper.find('.text-input input').setValue('text')
  expect(wrapper.find('.text-input input').element.value).toBe('text')
})

We mount the Form component with the mount method.

Then we get the input from the custom-input and call setValue to set its value.

Then we check the value of the input in the last line.

Slots

We can populate slots of a given component and test it.

For example, we can write:

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

const Layout = {
  template: `
    <div>
      <h1>Welcome!</h1>
      <main>
        <slot />
      </main>
      <footer>
        Thanks for visiting.
      </footer>
    </div>
  `
}

test('layout default slot', () => {
  const wrapper = mount(Layout, {
    slots: {
      default: 'Main Content'
    }
  })
  expect(wrapper.html()).toContain('Main Content')
})

We have the slots property in the object we pass into mount .

The default property populates the default slot.

Then we check that the rendered HTML has the 'Main Content' text.

Testing Named Slots

We can populate named slots and test its rendered content.

For example, we can write:

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

const Layout = {
  template: `
    <div>
      <header>
        <slot name="header" />
      </header>
      <main>
        <slot name="main" />
      </main>
      <footer>
        <slot name="footer" />
      </footer>
    </div>
  `
}

test('layout full page layout', () => {
  const wrapper = mount(Layout, {
    slots: {
      header: '<div>Header</div>',
      main: '<div>Main Content</div>',
      footer: '<div>Footer</div>'
    }
  })
  expect(wrapper.html()).toContain('<div>Header</div>')
  expect(wrapper.html()).toContain('<div>Main Content</div>')
  expect(wrapper.html()).toContain('<div>Footer</div>')
})

We have the Layout component with multiple slots.

Then in the test, we mount the components with all the slots filled.

The keys have the slot names, the values are the HTML we want to put inside it.

Then we can check the HTML that’s rendered in the last 3 lines.

Conclusion

We can test Vue 3 custom input components and slots with Vue Test Utils.