DEV Community

Cover image for 7 Tips to Enhance Your Unit Testing Skills with Vue
Alexander Opalic
Alexander Opalic

Posted on

7 Tips to Enhance Your Unit Testing Skills with Vue

Introduction

There's something particularly satisfying about writing unit tests, don't you agree? 🧐 Not only do they enhance the quality of your code, but they also serve as a form of documentation, helping newcomers navigate through the codebase with greater ease. Over the span of my six-year journey as a developer, I've had the privilege of gathering a wealth of tips and tricks for working with Vue test utils. And guess what? I can't wait to share these nuggets of wisdom with you! πŸ’‘While they are primarily intended for vue test utils, many of these tips can also be applied to other testing libraries. So buckle up and prepare for an enlightening ride through the captivating world of unit testing! πŸŽ’πŸš€

Harnessing the Power of Custom Code Snippets

Speed up your testing process and forget about memorizing code snippets! With VS Code, you can create your own custom snippets, tailored to your needs. For instance, if you find yourself frequently ensuring a wrapper does not contain any text, create a shortcut snippet like so:

expect(wrapper.text()).not.toBe('');
Enter fullscreen mode Exit fullscreen mode

Make it easy with a snippet texpTextNot that automatically populates this test line.

Here's how you define this snippet in VS Code:

"Expect Wrapper not to contain text": {
    "prefix": "texpTextNot",
    "body": [
        "expect(wrapper.text()).not.toBe('');"
    ],
    "description": "Wrapper should not contain text"
},
Enter fullscreen mode Exit fullscreen mode

Just type when vs code is open command p and than >snippets: Configure User Snippets

use console.log(wrapper.html())

Embrace the power of vue test utils wrapper.html() when unit testing. It generates a visual of your component's HTML output, helping you understand how it behaves under different conditions. This is like having a microscope to your component's structure. 🧐

Remember, your goal is to test the rendered HTML or Vue's virtual DOM, not methods in isolation. So let wrapper.html() be your secret weapon for better understanding your test cases. πŸ’ͺ
I personally use it all the time to get a better understanding of a complex vue component and see which, part it really renders.

Simplify Tests with the createWrapper Function

The createWrapper function is your helper for creating and configuring component wrappers consistently across tests. Check out this example with a ButtonComponent to see how it can simplify your tests and make them easier to understand and manage.

import Vue from 'vue'
import { shallowMount, ShallowMountOptions, Wrapper } from '@vue/test-utils'
import ButtonComponent from '~/components/ButtonComponent.vue'

const localVue = createLocalVue()

function createWrapper(options?: ShallowMountOptions<Vue>): Wrapper<Vue> {
  return shallowMount(BaseButton, {
    ...options,
    localVue,
    propsData: {
      label: 'Click me',
      color: 'blue',
    },
  })
}
// Rest of the test code...
Enter fullscreen mode Exit fullscreen mode

Focus on Behavior, Not Implementation for Robust Tests

Tests should focus on software's behavior, not its internal workings. Consider this example with a BaseButton:

it('emits a "buttonClicked" event when button is clicked', () => {
  const wrapper = shallowMount(BaseButton)

  wrapper.find('button').trigger('click')
  expect(wrapper.emitted('buttonClicked')).toBeTruthy()
})
Enter fullscreen mode Exit fullscreen mode

This test doesn't care about how the event is handled, just if the expected event was emitted when the button was clicked. Remember, test the interface, not the implementation, for more resilient tests. πŸ‘Œ

Keep Large Mock Data in Separate Files for Cleanliness

Don't let large mock data sets clutter your test files. Keep them in separate files like this:

// ./tests/mocks/userMock.js
export const userMock = {
  id: 1,
  name: 'Vegeta',
  email: 'vegeta@example.com',
  // and so on...
}
Enter fullscreen mode Exit fullscreen mode

Then import it in your test files. This approach makes your test files cleaner, improves maintainability, and helps other developers understand your tests better. 🌟

Meaningful Test Naming Conventions are Key

The right names for your tests greatly improve readability and maintainability. The "Should...When" or "Should...If" pattern is a popular choice in the testing community.

describe('Login Form', () => {
  it('should display an error message when the password is incorrect', () => {
    // Test code goes here...
  });

  it('should redirect to the dashboard when the login credentials are correct', () => {
    // Test code goes here...
  });
});
Enter fullscreen mode Exit fullscreen mode

With this pattern, your test suite serves as a form of documentation that quickly communicates what each test verifies. Remember, good test names are concise yet comprehensive and leave no ambiguity about the test's purpose. πŸŽ–οΈ

Keep Tests Self-Contained and Independent

It can be tempting to reuse setup, state, or even test results between tests to save some lines of code. However, this can lead to tests that are harder to understand and maintain, and more importantly, that may interfere with each other.

Each test should be an isolated scenario that doesn't depend on the state from previous tests. This means it should set up its own state, run the functionality it's testing, and make its assertions without relying on the results of other tests.

Here's a simple example:

// πŸ‘ Recommended
it('should increment the counter and update the DOM', async () => {
  const wrapper = mount(Counter)
  await wrapper.find('button').trigger('click')
  expect(wrapper.find('p').text()).toContain('1')
})

it('should decrement the counter and update the DOM', async () => {
  const wrapper = mount(Counter, {
    data: () => ({ count: 1 })
  })
  await wrapper.find('button').trigger('click')
  expect(wrapper.find('p').text()).toContain('0')
})

// πŸ‘Ž Not recommended
let wrapper
beforeEach(() => {
  wrapper = mount(Counter)
})

it('should increment the counter and update the DOM', async () => {
  await wrapper.find('button').trigger('click')
  expect(wrapper.find('p').text()).toContain('1')
})

it('should decrement the counter and update the DOM', async () => {
  await wrapper.find('button').trigger('click')
  expect(wrapper.find('p').text()).toContain('0') // This test depends on the previous one!
})

Enter fullscreen mode Exit fullscreen mode

The benefits of this approach include:

Predictability: Tests won't fail because of some overlooked state leaking from one test into another.

Readability: Each test is self-explanatory. You can read a single test and understand what it's doing without having to know about any other tests.

Parallelizability: Independent tests can run in any order, or even simultaneously, which can greatly improve test suite execution time.

Remember, in testing, simplicity and clarity are key! Self-contained tests might be a little longer, but they're a lot safer and more manageable in the long run. πŸ‘

Summary

I sincerely hope that this article has served up some fresh and useful tips to enhance your unit testing journey. Perhaps some of these insights were new, and have now become tools you can wield in your coding adventures. And remember, learning is a two-way street! πŸš€ I'm genuinely curious to learn from your experiences. If you have a favored tip or a unique approach that you'd love to share, please do! After all, in the grand arena of development, there's always space for improvement and infinite room for learning. πŸŒ πŸ’‘

Top comments (0)