Vue Unit Testing: The Breakdown

Tests must also pass and code be refactored

Vue Unit Testing: The Breakdown

So far so good. We have our application boilerplate and have successfully written our first test for the home page of our todo application.

Next, we have to get through fetching, displaying this information and manipulating it.

Let's use beloved axios to grab and interact with our API.

npm install --save axios

For our application, we'll use an already created database - TheGreenCodes data source. In the same manner of thinking as before, we create tests for the non-existent feature; listing to-do items.

We shall display uncategorised items on one page, all the incomplete items on another, and then follow and display those that have actually been completed on the last page.

As we are starting off, we ensure, just like before, that since we have nothing in our database, yet, we display an appropriate message. Among others, here are the questions we need to ask:

  • Does it show an appropriate message if no item exists in the database yet?
  • Does our application fetch to-do items if any?
  • Does the app show any error message in case it cannot get to the server?
  • Can I even create a task?

Bear with me on this one. In the spirit of TDD, we are going to write our application based on our tests. So from here on out, no UI until we have something complete. That is, after all, the whole point of testing; pushing code with the confidence it is not going to break.

Modify the test suit to watch for changes in our test files. That way, we do not have to run npm run test: unit after every modification.

Load your package.json and add the below:

...
"test:unit": "vue-cli-service test:unit",
"test:unit:watch": "vue-cli-service test:unit --watchAll", // line to add
...

From here, we open our console and run:

npm run test:unit:watch

Leave it open for now. You can always open it to see any changes that may occur. Let's fulfil our queries as we go.

We want our home page to display the 'empty database' message for an empty database. Let's test that.

To start, we do some modifications to our test suite, making it easier to work with as it grows.

import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'
import Vue from 'vue'


describe('Home.vue', () => {
// before any test case in this test suite, import and use bootstrap UI components 
  beforeEach(() => {
    Vue.use(BootstrapVue)
    Vue.use(BootstrapVueIcons)
  })
  it('Displays welcome message', () => {
    const wrapper = shallowMount(Home)
    const welcomeMessage = 'Welcome to TheGreenCodes awesome to-do list'
    expect(wrapper.text()).toMatch(welcomeMessage)
  })
)}

Along with the other previous imports, we import bootstrap UI components.

Remember:

Our test case is assuming a black box. We are testing this specific component. Nothing outside. So it does not know what we have. Without the use of the UI import, the test would surely work but will show a warning as to what b-container or b-card means. It doesn't know.

Let's test for a message in case there are no to-do items. We expect this to fail at the first instance as we have not implemented any feature to display to-do items.

Append the below code snippet to your suite:


  it('Displays a message when there are no todo items', () => {
    const wrapper = shallowMount(Home, {
      data () {
        return {
          todoItems: null
        }
      }
    })

    const titleFound = wrapper.find('h4')
    const emptyTodoListMessage = 'You have no existing todo!'
    expect(titleFound.text()).toMatch(emptyTodoListMessage)
  })

Reasoning

We mount the Home component and set a data property to hold the data from the API. We also grab the h4 tag, an element we expect to hold the message when there are no items on the to-do list.

We should also check that this 'You have no existing todo!' message does not display if there are indeed items on our to-do list.


  it('Does not display message when there are todo items', () => {
    const wrapper = shallowMount(Home, {
      data () {
        return {
          todoItems: [{ id: '1', title: 'new item', content: 'make awesome content' }]
        }
      }
    })
    expect(wrapper.find('h4').exists()).toBe(false)
  })

Run the tests, in case you have not done so yet, and take a look at the spectacular failure.

PS to self:

Stop logging 'madness' on Sentry servers.

Let's fix this.

<template>
  <div class="home">
    <h2>Welcome to TheGreenCodes awesome to-do list</h2>
    <div class="container">
      <b-card class="list-container">
        <h4 v-if="!todoItems">You have no existing todo!</h4>

      </b-card>
    </div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  name: 'Home',
  data () {
    return {
      todoItems: null
    }
  }
}
</script>

<style scoped>
.list-container{
    max-width: 170%;
}
</style>

As you may have noticed, we have redundant code. We create a new wrapper each time we have a test. We can have this centralized. Modify your test to look as below. We explain, as always what each means.

For brevity, we ignore other components of the test file.

...

describe("Home", () => {
  const build = () => {
    const wrapper = shallowMount(Home, {});
    return { wrapper };
  };
  beforeEach(() => {
    Vue.use(BootstrapVue);
    Vue.use(BootstrapVueIcons);

  });

...

At the very top of the description statement of the test, we shallowMount the Home component. That said, further tests can be used as below:


 it("Displays application title", () => {
    const { wrapper } = build();

    const welcomeMessage = "Welcome to TheGreenCodes awesome to-do list";
    expect(wrapper.text()).toMatch(welcomeMessage);
  });

  it("Displays a message when there are no todo items", () => {
    const { wrapper } = build({});

    wrapper.setData({
      todoItems: []
    });

    const titleFound = wrapper.find("h4");
    const emptyTodoListMessage = "You have no existing todo!";
    expect(titleFound.text()).toMatch(emptyTodoListMessage);
  });

With this refactor, we declare at the very start that it is the Home component in testing mode. We make the code cleaner and reduce having to remember that every time since we know that only the component's properties would be changing every time.

To set these individual properties, we can call the methods: setData, setMethods, setProps and so forth directly.

Because our component has no children, we navigated to shallowMount. However, if it did and we wanted to show whatever these children have in them as well, we could as well call mount and import it from the same path we got shallowMount.

What about the listing of all items?


// add imports
...
import axios from "axios";
import flushPromises from "flush-promises";


// more tests go here
...

it("Gets all todo items regardless of status", async () => {
    const { wrapper } = build();

    const expectedItems = {
      // ToDo: our items contain articles to read
      data: [
        {
          id: "1",
          title: "TheGreenCodes: Unit testing in Vue",
          content: "A guide to better predictable code.",
          complete: true
        },
        {
          id: "2",
          title: "TheGreenCodes: Tests must fail",
          content: "Building blocks to test driven development",
          complete: false
        },
        {
          id: "3",
          title: "TheGreenCodes: Tests must also pass",
          content: "This share",
          complete: false
        }
      ]
    };
    // on axios call, use expectedItems object
    jest.spyOn(axios, "get").mockResolvedValue(expectedItems);

    await wrapper.vm.getToDoItems();
    // make sure API request promises are complete before looking for articles to read
    await flushPromises();

    expect(axios.get).toHaveBeenCalledTimes(1);
    expect(axios.get).toHaveBeenCalledWith("todos/");

    expect(wrapper.vm.todoItems).toEqual(expectedItems.data);

    // Finally, we make sure we've rendered the content from the API.
    const todos = wrapper.findAll('[data-test="todo"]');
    expect(todos).toHaveLength(3);


    // we have articles now
    expect(wrapper.html().includes("You have no existing todo!")).toBe(false);
    expect(wrapper.html().includes("TheGreenCodes: Unit testing in Vue")).toBe(
      true
    );
  expect(
      wrapper.html().includes("Building blocks to test driven development")
    ).toBe(true);
  });

Through this case, we build an object we expect to get as a response. It returns a list of the series you are reading right now.

We say, ' Whenever you call on axios with a get request, return data that looks similar to this - '

Since our component calls the getToDoItems method, we can call it directly by using await wrapper.vm.getToDoItems(). This is almost similar to how we edited or set our data before.

Because the call is asynchronous, i.e, a request is made to the server and data sent back, we have to ensure our data replicates a wait time as well. For this, we call to flushPromises after adding it to the list of imports.

We are also ascertain that the get request is made only once and to the specific URL we need it to.

As axios will return the information wrapped in an inner data object, we want our data to get out of that to be compared directly to an array we intend to store. Thus the line:

expect(wrapper.vm.todoItems).toEqual(expectedItems.data);

For the last part, this should come out clearly.

If you have seen the light at the end, you might have seen the errors all snarly and already changed your Home component. For reference:

<template>
  <div class="home">
    <h2>Welcome to TheGreenCodes awesome to-do list</h2>
    <div class="container">
      <b-card class="list-container">
        <h4 v-if="todoItems.length < 1" class="empty-list">
          You have no existing todo!
        </h4>
        <div v-else>
          <b-list-group>
            <b-list-group-item
              v-for="(item, index) in todoItems"
              :key="index"
              data-test="todo"
            >
              <h5 class="d-flex justify-start">
                {{ item.title }}
              </h5>

              <article class="d-flex justify-start">
                {{ item.content }}
              </article>
            </b-list-group-item>
          </b-list-group>
        </div>
      </b-card>
    </div>
  </div>
</template>

<script>
import axios from "axios";
export default {
  name: "Home",
  data() {
    return {
      todoItems: [],
      errorFound: null,
    };
  },
  created() {
    this.getToDoItems();
  },
  methods: {
    getToDoItems() {
      axios
        .get("todos/")
        .then((res) => {
          this.todoItems = res.data;
        })
        .catch((error) => {
          this.errorFound = error;
        });
    },
  },
};
</script>

<style scoped>
.list-container {
  max-width: 170%;
}
</style>

This change should make our current tests pass.

Zoom in on this line:

...
<b-list-group-item
              v-for="(item, index) in todoItems"
              :key="index"
              data-test="todo" 
            >
...

To be specific data-test="todo".

This is a property added to the list of items for the articles yet to be read. This helps us call:

 const todos = wrapper.findAll('[data-test="todo"]');

You can equate this to a CSS class that is relevant to writing unit tests. We all the items in one swoop and check their length.

We can go on with the tests and features but this is the baseline;

  • Write your failing tests
  • Make the tests pass
  • Refactor the code.

Working iteratively through this , enables not just error-free code, but also prevents us from writing extra redundant code.

FeatureComplete.png

Over the lifetime of the repository,awesome-to-do, we shall iterate through this process and come up with a complete application. I invite both you and your counterpart in crime, to join in and build better software together.

Alas! Let the OpenSource contributions begin.

Regards,

Marvin K.

PS: Picture creds: monkeyuser.com