Five Traps to Avoid While Unit Testing Vue.js

Dec 20, 2017 | Aurélien Bottazini

Unit testing is a skill on its own, and it may not be your priority while you are learning a new language or a new framework. When you begin unit testing your components, you may feel a bit lost, and the following questions may come to your mind:

  • How do I test?
  • What should I test?
  • What are the traps to avoid?
    • waiting until the end
    • testing the wrong things
    • test doubles
    • structural coupling
    • testing everything
  • Are there patterns to follow?

In the following, I will show you concrete steps you can use in your day-to-day work, walking through Vue.js examples.

The First Trap: Waiting Until the End

As you practice testing, with the correct dev environment, it will become enjoyable; I promise you. But you have a lot to learn to get there. You are still figuring out that new framework, and, on top of that, those annoying co-workers of yours insist on adding tests. The whole test thing is slowing you down.

Even for your very first component, the longer you wait, the harder it is going to be to write your component unit test. That's because advanced unit testers write their code and consider testing at the same time. As they write their code, they make it easy to test. Testable code is a sign of a good software architecture1.

Writing the first test will slow you down, iterating on it will be exponentially faster. That first test gives you confidence that your code still works while refactoring.

The first thing you should do is to make sure you have a working running environment.

import { shallow } from 'vue-test-utils';
import YourComponent from './YourComponent.vue';

describe('Your Component', () => {
  it('renders a vue instance', () => {
    expect(shallow(YourComponent).isVueInstance()).to.be.true;
  });
});

This first test is very important. You want to make sure your component renders whatever you throw at it. After all, your components are going to use data coming from various sources. You should consider those various sources as unsafe, and you should not trust they are going to provide valid data to your components.

With this first simple test, I am making sure that my component does not crash completely with empty data.

It is important to start with shallow as your rendering mechanism. Shallow will only render your specific component and not child components. It makes your tests more robust, as you are only focusing on one component at a time.

The Second Trap: Testing the Wrong Things

This second trap follows the first one almost immediately. It is easy to get confused and become stuck trying to test part of your code that you should not test.

You don't want to test the internal of your component and that a specific function was called. Those are details; you want to focus on the big picture. If you focus on those details, your tests are going to be coupled to your current implementation, and refactoring your code will force you to refactor your tests at the same time. Tests should help you to refactor. If you need to modify your tests all the time when you refactor, it probably means you are testing the wrong things.

Here is an example of a bad test. I am testing that a specific function from my component is called when we click on a button. With this test, if I modify my code—for example if I decide to rename the function—I have to modify both my component and my component test too.

import { shallow } from 'vue-test-utils';
import ExampleComponent from '../ExampleComponent.vue';
import Sinon from 'sinon';

describe('Bad Example', () => {
  it('Do not do this: should toggle the visibility', () => {
    const toggleParagraphDisplaySpy = Sinon.spy(
      ExampleComponent.methods,
      'toggleParagraphDisplay'
    );

    const wrapper = shallow(ExampleComponent);
    wrapper.find('button').trigger('click');

    expect(toggleParagraphDisplaySpy.calledOnce);

    toggleParagraphDisplaySpy.restore();
  });
});

So what should you test?

You should test your component's public interface 2 and side effects.

Testing the public interface

You want to test that, when passing specific props to your components, you get a specific output. For components, this output is the rendered markup.

  • You can test that a class is present on your element:

    import { shallow } from 'vue-test-utils';
    import MyComponent from './MyComponent.vue';
    
    describe('MyComponent', () => {
      it('has a active class by default', () => {
        expect(shallow(MyComponent, {
          propsData: {
            isActive: true }}).hasClass('active')).to.be.true;
      });
    });
    
  • You can test if an element is rendered:

    import { shallow } from 'vue-test-utils';
    import MyComponent from './MyComponent.vue';
    
    describe('MyComponent', () => {
      it('contains a list', () => {
        expect(shallow(MyComponent).contains('ul')).to.be.true;
      })
    })
    
  • You can make sure child components are present. In the following example, I have added a specific class to my stubbed component. This way, I am sure I won't grab other elements by mistake:

    import { shallow } from 'vue-test-utils';
    import MyComponent from './MyComponent.vue';
    
    describe('MyComponent', () => {
      it('has child components', () => {
        expect(shallow(MyComponent, {
          propsData: {
            children: [1, 2, 3, 4 ,5],
          },
          stubs: {
            'ChildComponent': '<li class="my-stub"></li>',
          },
        }).findAll('li.my-stub').length).to.equal(5);
      })
    })
    

Testing side effects

You may want to test that when an event occurred on your component—clicking on a button, scrolling with your mouse—a certain side effect happens. This side effect can be something like:

  • an HTTP call:

    import { shallow } from 'vue-test-utils';
    // This can be any http library, axios, fetch…
    import http from 'http';
    import MyComponent from './MyComponent.vue';
    
    describe('MyComponent', () => {
      it('gets google.com', () => {
        sinon.spy(http, 'get');
        const wrapper = shallow(MyComponent);
        wrapper.find('button').trigger('click');
        expect(http.get.withArgs('http://www.google.com').calledOnce).to.be.true;
        http.restore();
      });
    })
    
  • toggling the visibility of a UI element:

    import { shallow } from 'vue-test-utils';
    // This can be any http library, axios, fetch…
    import http from 'http';
    import MyComponent from './MyComponent.vue';
    
    describe('MyComponent', () => {
      it('disappears', () => {
        const wrapper = shallow(MyComponent);
        expect(wrapper.contains('div.foo')).to.be.true;
        wrapper.find('button').trigger('click');
        expect(wrapper.contains('div.foo')).to.be.false;
      });
    })
    

The Third Trap: Test Doubles

"Test double" is the generic term for any kind of pretend object used in place of a real object for testing purposes 3. There are different kind of test doubles, from the simplest dummy to the complete mock.

Here is the vocabulary I like to use when talking about test doubles

  • A Test Double is abstract.
  • A Dummy has all the methods but does nothing. This allows you to make your code work in your testing scenarios by passing empty arguments to your functions.
  • A Stub is like a Dummy, except it returns a value instead of null or undefined or 0. A Stub allows you to choose a pathway of execution in your code
  • A Spy is like a Dummy, except it remembers what was called.
  • A Mock is like a Spy, except it has a verify method on it. It checks that everything was called in the right order.
  • A Fake is a fake/custom implementation. It allows you to bypass things like authentication, databases, or other expensive resources. I find it particularly useful with integration tests.

    The following diagram shows how test doubles relate to each other.

How do I use test doubles?

It is useful to use a framework for the purpose of creating spys and mocks. I like SinonJS because it uses the same test double vocabulary from the previous section.

Now that I have encumbered your mind with all this new vocabulary, I do have good news. For the simplest test doubles—which are the ones you are going to use most often—you don't have to use a framework. It is far simpler to just write an empty function returning what you want in that particular case.

For example, for a component using Vuex to keep track of the user login state:

import { shallow, createLocalVue } from 'vue-test-utils';
import Vuex from 'vuex';
import MyComponent from './MyComponent';

describe('MyComponent for a logged in User', () => {
  it('shows the user ID', () => {
    const localVue = createLocalVue();
    localVue.use(Vuex);

    const store = new Vuex.Store({
      getters: {
        isLoggedIn: () => true,
      }
    });

    const wrapper = shallow(MyComponent, {
      localVue,
      store,
    });

    expect(wrapper.text()).contains('Thanks for coming back!');
  });
})

Augmenting a previous example, we can use spies to check for function calls:

import { shallow } from 'vue-test-utils';
import MyComponent from './MyComponent';

describe('MyComponent', () => {
  it('gets google.com and save analytics', () => {
    sinon.spy(http, 'get');
    sinon.spy(http, 'post');
    const wrapper = shallow(MyComponent);
    wrapper.find('button').trigger('click');

    expect(http.get.withArgs('http://www.google.com').calledOnce).to.be.true;
    expect(http.post.withArgs('http://www.analytics.com').calledOnce).to.be.true;
    http.restore();
  });
});

Mocks are a bit different. You define an expected behavior first, and then you verify it:

import { shallow } from 'vue-test-utils';
import MyComponent from './MyComponent';

describe('MyComponent', () => {
  it('gets google.com', () => {
    mock = sinon.mock(http);
    mock.expects('get').withArgs('http://www.google.com').once;

    const wrapper = shallow(ExampleComponent);
     wrapper.find('button').trigger('click');

     mock.verify();
     mock.restore();
   });
});

There is one thing to keep in mind: Do not forget to restore the original function. This is called un-mocking. Otherwise, your following tests may interfere with each other by using test doubles when they should not.

The Fourth Trap: Structural Coupling

You don't want to have one test file per component. If you do, your test structure becomes a mirror of your code structure. This will make it harder to refactor your code and to switch between implementation strategies. This is called structural
coupling
and is best illustrated with an example.

Let's imagine we are creating a Button component. It begins with a simple button that sends some event on clicks. We start with one component and one component test shallow rendering that button. Shallow rendering means the test won't render child components. We can focus the test on that particular component without testing the behavior of child components.

Then we realize that we want to do more things with the component. Send different actions based on props passed down, do some initialization actions. Being good unit test citizens, we decide to split this Button component.

This works. Our components are covered by one unit test file for each. But do you see the problem here? The tests are mimicking the structure of the code.

If for any reason we decide to change the structure of the code—add or remove files—we will need to do the same adjustments with our tests. It will slow down the whole process.

Consider this setup instead:

We are using mount as the rendering method. mount means child components are going to be rendered too. It allows us to test all the desired behavior of our button component without tying our tests to the structure of the code. For example, if we decided to refactor to switch to another kind of polymorphism for those components, we won't have to change our tests.

The Fifth Trap: Testing Everything

The initial idea is you want to test everything. You want to test that your component renders properly, that your events works as desired, that your asynchronous code is set up correctly, etc.

Testing boundaries—http calls, connections to external services—can be tricky to do and complicate your test suite pretty quickly.

I would like you to consider something completely different: not testing. Here comes the Humble Object4 pattern.

The root component is Humble. It is humble because it does not contain any domain logic. It is only responsible for calling an api to fetch some data, and it passes down that data to some child components.

If your component is humble, you don't need to unit test it. You can test the child components and test all the domain logic there. After your User Story is complete, you can write an integration test to make sure everything is wired up correctly.

Hope you enjoyed reading about Unit test traps. Follow us @dox_engineering if you'd like to be notified of updates to this blog.


Thank you Bruno, Jey, James and Aaron for reading drafts and offering advice.
Thank you Hannah Frank for the illustration.

Be sure to follow @doximity_tech if you'd like to be notified about new blog posts._


  1. Robert Cecil Martin. 2017. Clean Architecture: A Craftsman's
    Guide to Software Structure.
     

  2. https://vue-test-utils.vuejs.org/en/guides/common-tips.html 

  3. Gerard Meszaros. 2007. xUnit Test Patterns: Refactoring Test Code. 

  4. http://xunitpatterns.com/Humble%20Object.html