Chapter 5 – Testing Vue.js Components with Jest

Chapter 5

Test Computed Properties and Watchers

Computed properties and watchers are the reactive parts of the logic of Vue.js components. They each serve totally different purposes, that is, one is synchronous and the other is asynchronous, which makes them behave slightly differently.

In this chapter, we'll go through the process of testing them, and we'll see what different cases we can find along the way.

Computed Properties

Computed properties are simple reactive functions that return data in another form. They behave exactly like the language-standard get/set properties:

class X {

  get fullName() {

    return `${this.name} ${this.surname}`;

  }

  set fullName(value) {

    // ...

  }

}

When you're using plain objects, then it'd be as follows:

export default {

  computed: {

    fullName() {

      return `${this.name} ${this.surname}`;

    }

  }

};

You can even add the set property as follows:

export default {

  computed: {

    fullName: {

      get() {

        return `${this.name} ${this.surname}`;

      },

      set(value) {

        // ...

      }

    }

  }

};

Testing Computed Properties

Testing a computed property is very simple. Sometimes, you probably won't test a computed property exclusively, but instead, you'll test it as part of other tests. However, most of the time it's good to have a test for it; whether that computed property is cleaning up an input or combining data, we want to make sure things work as intended. So, let's begin.

First of all, create a Form.vue component:

<template>

  <div>

    <form>

      <input type="text" v-model="inputValue">

      <span class="reversed">{{ reversedInput }}</span>

    </form>

  </div>

</template>

<script>

export default {

  props: ["reversed"],

  data: () => ({

    inputValue: ""

  }),

  computed: {

    reversedInput() {

      return this.reversed ?

        this.inputValue.split("").reverse().join("") :

        this.inputValue;

    }

  }

};

</script>

It will show an input and, next to it, you'll see the same string but reversed. It's just a silly example, but enough to test it.

Now, add it to App.vue, then put it after the MessageList component, and remember to import it and include it within the components component option. Then, create a test/Form.test.js file with the usual bare bones we've used in other tests:

import { shallowMount } from "@vue/test-utils";

import Form from "../src/components/Form";

describe("Form.test.js", () => {

  let cmp;

  beforeEach(() => {

    cmp = shallowMount(Form);

  });

});

Now, create a test suite with two test cases:

describe("Properties", () => {

  it("returns the string in normal order if reversed property is not true", () => {

    cmp.setData({ inputValue: "Yoo" });

    expect(cmp.vm.reversedInput).toBe("Yoo");

  });

  it("returns the reversed string if reversed property is true", () => {

    cmp.setData({ inputValue: "Yoo" });

    cmp.setProps({ reversed: true });

    expect(cmp.vm.reversedInput).toBe("ooY");

  });

});

We can access the component instance within cmp.vm so that we can access the internal state, computed properties, and methods. Then, to test it is just about changing the value and making sure it returns the same string when reversed is false.

For the second case, it is almost the same, with the only difference being that we must set the reversed property to true. We could navigate through cmp.vm... to change it, but vue-test-utils give us a helper method, setProps({ property: value, ... }), that makes it very easy.

That's it; depending on the computed property, it may need more test cases.

Watchers

Honestly, I haven't come across any test case where I really need to use watchers that my computed properties couldn't solve. I've seen them misused as well, leading to a very unclear data workflow among components and messing everything up. So, don't rush into using them, and think beforehand.

As you can see in the Vue.js docs (https://vuejs.org/v2/guide/computed.html#Watchers), watchers are often used to react to data changes and perform asynchronous operations, such as performing an ajax request.

Testing Watchers

Let's say that we want to do something when the inputValue from the state change. We could perform an ajax request, but since that's more complicated (and we'll cover it in more detail in the next lesson), let's just use a console.log function. Add a watch property to the Form.vue component options:

watch: {

  inputValue(newVal, oldVal) {

    if (newVal.trim().length && newVal !== oldVal) {

      console.log(newVal)

    }

  }

}

Notice the inputValue watch function matches the state variable name. By convention, Vue will look it up in both the properties and data states by using the watch function name, in this case, inputValue, and since it will find it in data, it will add the watcher there.

Note that a watch function takes the new value as the first parameter and the old one as the second. In this case, we've chosen to log only when it's not empty and the values are different. Usually, we'd like to write a test for each case, depending on the time you have and how critical that code is.

So, what should we test about the watch function? Well, that's something we'll also discuss further in the next lesson when we talk about testing methods, but let's say we just want to know that it calls console.log when it should. So, let's add the bare-bones of the Watchers test suite within Form.test.js, as follows:

describe("Form.test.js", () => {

  let cmp;

  describe("Watchers - inputValue", () => {

    let spy;

    beforeAll(() => {

      spy = jest.spyOn(console, "log");

    });

    afterEach(() => {

      spy.mockClear();

    });

    it("is not called if value is empty (trimmed)", () => {

      // TODO

    });

    it("is not called if values are the same", () => {

      // TODO

    });

    it("is called with the new value in other cases", () => {

      // TODO

    });

  });

});

Here, we're using a spy on the console.log method, initializing it before starting any test, and then resetting its state after each of them so that they start from a clean spy.

To test a watch function, we just need to change the value of what's being watched, in this case, the inputValue state. But there is something curious... let's start from the last test:

it("is called with the new value in other cases", () => {

  cmp.vm.inputValue = "foo";

  expect(spy).toBeCalled();

});

Here, we changed the inputValue, so the console.log spy should be called, right? Well, it won't. But wait, there is an explanation for this: unlike computed properties, watchers are deferred to the next update cycle that Vue uses to look for changes. So, basically, what's happening here is that console.log is indeed called, but after the test has finished.

Notice that we're changing inputValue in the raw way by accessing the vm property. If we wanted to do it this way, we'd need to use the vm.$nextTick (https://vuejs.org/v2/api/#vm-nextTick) function to defer code to the next update cycle:

it("is called with the new value in other cases", done => {

  cmp.vm.inputValue = "foo";

  cmp.vm.$nextTick(() => {

    expect(spy).toBeCalled();

    done();

  });

});

Notice here that we called a done function that we received as a parameter. That's one way Jest (https://jestjs.io/docs/en/asynchronous.html) can test asynchronous code.

However, there is a much better way. The methods that vue-test-utils give us, such as emitted or setData, take care of that under the hood. This means that the last test can be written in a cleaner way just by using setData:

it("is called with the new value in other cases", () => {

  cmp.setData({ inputValue: "foo" });

  expect(spy).toBeCalled();

});

We can also apply the same strategy for the next one, with the only difference being that the spy shouldn't be called:

it("is not called if value is empty (trimmed)", () => {

  cmp.setData({ inputValue: " " });

  expect(spy).not.toBeCalled();

});

Finally, testing that it is not called if the values are the same is a bit more complex. The default internal state is empty; so, first, we need to change it, wait for the next tick, then clear the mock to reset the call count, and change it again. Then, after the second tick, we can check the spy and finish the test.

This would be much simpler if we recreated the component at the beginning, overriding the data property. Remember, we can override any component option by using the second parameter of the mount or shallowMount functions:

it("is not called if values are the same", () => {

  cmp = shallowMount(Form, {

    data: () => ({ inputValue: "foo" })

  });

  cmp.setData({ inputValue: "foo" });

  expect(spy).not.toBeCalled();

});

Wrapping Up

In this chapter, you've learned how to test part of the logic of Vue components: computed properties and watchers. We've gone through different example test cases that we could come across while testing both of them. You've also learned about some of the Vue internals, such as the nextTick update cycle.

You can find the code for this chapter on GitHub (https://github.com/alexjoverm/vue-testing-series/tree/Test-State-Computed-Properties-and-Methods-in-Vue-js-Components-with-Jest).