Chapter 4. Testing component methods

This chapter covers

  • Testing component methods
  • Testing code that uses timer functions
  • Using mocks to test code
  • Mocking module dependencies

A Vue application without methods is like a smoothie without fruit. Methods contain juicy logic that adds functionality to Vue components, and that logic needs to be tested.

Testing self-contained methods isn’t complicated. But real-world methods often have dependencies, and testing methods with dependencies introduces a world of complexity.

A dependency is any piece of code outside the control of the unit of code that’s under test. Dependencies come in many forms. Browser methods, imported modules, and injected Vue instance properties are common dependencies that you’ll learn to test in this chapter.

To learn how to test methods and their dependencies, you’re going to add start and stop methods to the ProgressBar component in the Hacker News application (figure 4.1). These methods will start and stop the progress bar running. To make the progress bar increase in width over time, the component will use timer functions, so you’ll need to learn how to test code that uses timer functions.

Figure 4.1. The finished progress bar at 80% complete

After you’ve added methods to the ProgressBar component, you’ll refactor the application to fetch data in the ItemList component and use the progress bar to indicate that the data is being fetched. These tasks will introduce challenges like testing that a function was called, testing asynchronous code, and controlling the behavior of module dependencies—code that’s imported into one module from another.

You’ll start by writing tests for the ProgressBar methods.

4.1. Testing public and private component methods

In Vue, you can add functionality to components by creating component methods. Component methods are useful places to write logic that is too complex for the component template.

Often, components use methods internally, like logging a message to the console when a button is clicked, as shown in the next listing.

Listing 4.1. Calling a method on click
<template>
  <button @click="logClicked" /> //  1
</template>

<script>
export default {
  methods: {
    logClicked() { //                2
      console.log("clicked");
    }
  }
};
</script>

  • 1 Calls logClicked when the button is clicked
  • 2 Defines the logClicked method

You can think of these as private methods—they aren’t intended to be used outside of the component. Private methods are implementation details, so you don’t write tests for them directly.

Alternatively, you can create methods, such as that shown in the next listing, that are intended to be used by other components. You can think of these as public methods.

Listing 4.2. Creating a public method
const vm = new Vue({      1
  methods: {
    logHello() {
      console.log("hello");
    }
  }
});

vm.logHello()             2

  • 1 Creates a Vue instance with the logHello method
  • 2 Calls logHello from outside the component

Public methods are a less common pattern in Vue, but they can be powerful. You should always write tests for public methods, because public methods are part of the component contract.

Note

You’re following on from the app you made in chapter 3. If you don’t have the app, you can check out the chapter-4 Git branch following the instructions in appendix A.

In the previous chapter you created a ProgressBar component. Right now it’s just a static component, but in this chapter you’ll add public methods so that you can control it from other components in the application.

The ProgressBar component will have three methods: start, finish, and fail. The start method will set the progress bar running, the finish method will hide the bar, and the fail method will put the bar into an error state.

Note

To avoid repetition, in this chapter you’ll implement only the ProgressBar start and finish methods. I’ll leave it to you to implement the fail method in the exercises at the end of the chapter. You can see the fully implemented fail method in the chapter-7 Git branch.

To write tests for the progress bar you need to learn how to test public methods and how to write tests for code that use timer functions.

4.1.1. Testing public component methods

The process of testing public methods is simple: you call the component method and assert that the method call affected the component output correctly.

Imagine you have a pop-up component that exposes a hide method. When the hide method is called, the component should have its style.display property set to none. To test that the hide method worked correctly, you would mount the component, call the hide method, and check that the component has a style.display value of none, as shown in the following code.

Listing 4.3. Testing a public method
test('is hidden when hide is called', () => {
  const wrapper = shallowMount(Popup)                    1
  wrapper.vm.hide()                                      2
  expect(wrapper.element.style.display).toBe('none')     3
})

  • 1 Mounts the component
  • 2 Calls the public hide method on the component instance
  • 3 Asserts that the wrapper root element has a display none style

This is how you test public methods: you call the method and assert that the component output is correct. The tests you write for the ProgressBar component will follow this pattern.

The specs for ProgressBar follow:

  • ProgressBar should display the bar when start is called.
  • ProgressBar should set the bar to 100% width when finish is called.
  • ProgressBar should hide the bar when finish is called.
  • ProgressBar should reset the width when start is called.

These tests will be pretty simple, because they are self-contained methods without any dependencies. The first test will check that the root element removes a hidden class when start is called. You can check that it doesn’t have a class by using the Vue Test Utils classes method and the Jest not modifier. The not modifier negates an assertion as follows:

expect(true).not.toBe(false)

You have an existing test that checks that the bar initializes with a hidden class. You can replace that with the new test that checks that it initializes with a hidden class and removes it when start is called. Add the code from the next listing to src/components /__tests__/ProgressBar.spec.js.

Listing 4.4. Testing component state
test('displays the bar when start is called', () => {
  const wrapper = shallowMount(ProgressBar)
  expect(wrapper.classes()).toContain('hidden')            1
  wrapper.vm.start()                                       2
  expect(wrapper.classes()).not.toContain('hidden')        3
})

  • 1 Asserts that the hidden class exists
  • 2 Triggers the test input by calling the start method on the component instance
  • 3 Asserts that the hidden class was removed

When you run the test with npm run test:unit, the test will display an error because start is not a function. This isn’t one of those friendly assertion errors that you know and love; it’s a useless type error.

Type errors don’t aid you in the quest for a failing assertion. You should stop type errors like this by adding boilerplate code to the unit you’re testing. In src/-components/ProgressBar.vue add a <script> block with a methods object of empty methods in the component options, as follows:

<script>
export default {
  methods: {
    start() {},
    finish() {}
  }
}
</script>

Now run the test again with npm run test:unit to check that it fails with a descriptive assertion error. When you’ve seen an assertion error, you can make the test pass by adding the code from the next listing to src/components/ProgressBar.vue.

Listing 4.5. ProgressBar.vue
<template>
  <div
    :class="{               1
    hidden: hidden
  }"
  :style="{
    'width': '0%'
  }"/>
</template>

<script>
export default {
  data() {
    return {
      hidden: true
    }
  },
  methods: {                2
    start () {
      this.hidden = false   3
    },
    finish() {}
  }
}
</script>

  • 1 Defines a dynamic hidden class that is added to element if hidden is true
  • 2 Defines the component methods
  • 3 Sets the instance state inside the start method

Check that the test passes: npm run test:unit. Great—now the ProgressBar root element removes the hidden class when start is called. Next you want to test that calling finish sets the progress bar width to 100% and hides the bar. This will make the progress bar appear to finish loading and then disappear.

You’ll write two tests—one to check that the width is set to 100% and one to check that the hidden class is added. You can use the same approach that you did in the earlier test—call the method and then assert against the rendered output. Add the following code to src/components/__tests__/ProgressBar.spec.js.

Listing 4.6. Testing public methods
test('sets the bar to 100% width when finish is called', () => {
  const wrapper = shallowMount(ProgressBar)
  wrapper.vm.start()                                  1
  wrapper.vm.finish()                                 2
  expect(wrapper.element.style.width).toBe('100%')    3
})

test('hides the bar when finish is called', () => {
  const wrapper = shallowMount(ProgressBar)
  wrapper.vm.start()
  wrapper.vm.finish()
  expect(wrapper.classes()).toContain('hidden')
})

  • 1 Puts the component in a dirty state by calling start
  • 2 Triggers the test input by calling the finish method on the component instance
  • 3 Asserts that the element has a width of 100%

Check that the tests fail with an assertion error. To make the tests pass, you need render the width using a percent value and reset the state in the finish method.

To do that, you can add a percent property to the component and use it to render the width. In src/components/ProgresBar.vue, update the data method to return an object with percent set to 0 using the following code:

data() {
    return {
      hidden: true,
      percent: 0
    }
  },

In the same file, update the width style in the <template> block to use the percent value as follows:

'width': `${percent}%`

Finally, replace the finish method in src/components/ProgressBar.vue with the following code:

finish() {
  this.hidden = true
  this.percent = 100
}

Make sure the tests pass: npm run test:unit.

Now you have a finish method that sets the width to 100% and a start method that will start the component running from 0%. The component is going to start and finish multiple times during the application lifecycle, so start should reset the ProgressBar to 0% when it’s called.

You can check this by calling the finish method to set the width to 100%, and then calling the start method and asserting that the width is reset to 0%.

Add the following code to the describe block in src/components/__tests__/ProgressBar.spec.js:

test('resets to 0% width when start is called', () => {
  const wrapper = shallowMount(ProgressBar)
  wrapper.vm.finish()
  wrapper.vm.start()
  expect(wrapper.element.style.width).toBe('0%') //
})

The test will fail because finish sets the width to 100 and start doesn’t reset it. To make the test pass, you need to update the ProgressBar start method. Add the following code to the start method in src/components/ProgressBar.vue:

this.percent = 0

Run the test again: npm run test:unit. The test will pass—nice.

These are the kind of tests I love—small, self-contained, and easy to understand. With tests like these, you’re free to refactor the implementation of the methods however you like, as long as the component maintains its contract and generates the correct output.

As you can see, these kinds of tests are nice and simple: provide an input, call a method, and then assert the rendered output. The real complexity of testing methods is when methods have dependencies, like timer functions. To test that the progress bar increases in width over time you need to learn how to test timer functions.

4.2. Testing timer functions

Timer functions are JavaScript asynchronous functions and their counterparts, like setTimeout and clearTimeout. Timer functions are a common feature in Java-Script applications, so you need to be able to test code that uses them. But timer functions run in real time, which is bad news for speed-sensitive unit tests.

Note

If you’re not familiar with timer functions like setTimeout, I recommend reading this great introduction in the Node docs—https://nodejs.org/en/docs/guides/timers-in-node.

Unit tests should run faster than Usain Bolt sprints 100 meters. Every extra second a unit test takes to run makes the test suite worse, so testing code that uses timer functions can be problematic. Think about it. If you want to test that a component does something after 500 ms using setTimeout, then you would need to wait for 500 ms in the test. This delay would hammer the performance of a test suite, which usually runs hundreds of tests in a few seconds.

The only way to test timer functions without slowing down tests is by replacing the timer functions with custom functions that can be controlled to run synchronously. One of the great features of JavaScript (or worst, depending who you ask!) is how malleable it is. You can easily reassign global variables, as follows:

setTimeout = () =>{ console.log('replaced') }

You could replace timer functions with functions that behave like timer functions but that use a method to control the time and run the timer functions synchronously. Functions like this that are created to replace existing functions in tests are known as mock functions.

It would be complicated to replace the timer functions with mock functions yourself, but you can use a library to replace them for you. The Jest testing framework that you’re using is a kitchen sink framework. It has almost all the features you need to test JavaScript, without the need to reach for other libraries. One useful Jest feature is fake timers.

4.2.1. Using fake timers

Fake timers are mock functions that replace global timer functions. Without fake timers, testing code that uses timer functions would be horrendous.

In the Hacker News app, the ProgressBar component will use the setInterval timer function to increment its width over time, so you need to use fake timers to test it.

Note

setInterval is a timer function that executes a callback function at a regular interval.

You can use lots of libraries to mock fake timers, but in this book I’ll show you how to use Jest fake timers. Jest fake timers work by replacing the global timer functions when you call the jest.useFakeTimers method. After you’ve replaced the timers, you can move the fake time forward using a runTimersToTime, as shown in the next listing.

Note

The jest object is a global object added by Jest when it runs the tests. The jest object includes lots of test utility methods, like fake timers, that you’ll use in this chapter.

Listing 4.7. Using fake timers
jest.useFakeTimers()                                 1
setTimeout(() => console.log('100ms are up'), 100)   2
jest.runTimersToTime(100) // logs 100ms are up       3

  • 1 Replaces the global timer functions with Jest implementations
  • 2 Adds setTimeout to fire after 100 ms
  • 3 Moves the fake clock forward 100 ms, which will cause the setTimeout callback to run

The safest way to use fake timers in a test suite is to call useFakeTimers before each test runs. That way, the timer functions will reset before every test.

You can run functions before each test by using the beforeEach hook. This hook is useful for performing setup for tests.

Because you’re going to write tests for code that uses a timer function, you should add a beforeEach hook to enable fake timers in the ProgressBar test file. Add the code in the following listing to the top of the describe block in src/components/__tests__/ProgressBar.spec.js.

Listing 4.8. Calling useFakeTimers before each test
beforeEach(() => {            1
  jest.useFakeTimers()        2
})

  • 1 Function to run before each test
  • 2 Replaces global timer functions

The progress bar will appear to load by increasing its width 1% every 100 ms after the start method is called. You can test this by calling the ProgressBar start method, and then moving the fake time forward and asserting that the width has increased by the expected amount. You should add a few assertions to make sure the time increments correctly.

Add the code from the next listing to the describe block in src/components/__tests__/ProgresBar.spec.js.

Listing 4.9. Moving the time forward with fake timers
  test('increases width by 1% every 100ms after start call', () => {
    const wrapper = shallowMount(ProgressBar)
    wrapper.vm.start()
    jest.runTimersToTime(100)                            1
    expect(wrapper.element.style.width).toBe('1%')       2
    jest.runTimersToTime(900)                            3
    expect(wrapper.element.style.width).toBe('10%')
    jest.runTimersToTime(4000)
    expect(wrapper.element.style.width).toBe('50%')
})

  • 1 Moves the global time forward 100 ms, and fires any timer callback that is scheduled to run after 100 ms
  • 2 Asserts that the wrapper element has the correct style
  • 3 Moves the time forward again by 900 ms; note that the total time now elapsed is 1,000 ms

Now, run the test and watch it fail: npm run test:unit. To make the test pass, you need to use setInterval in the ProgressBar start method.

You already have code that uses the percent property to set the width of the root element. So you just need to update the start method to change the percent value every 100 ms. You’ll also save the timer ID returned by setInterval, so that you can clear the interval in a later test. Open src/components/ProgressBar.vue and edit the start method to include the following code.

Listing 4.10. Using a timer function in a component
start () {
  this.hidden = false
  this.percent = 0
  this.timer = setInterval(() => {     1
    this.percent++                     2
  }, 100)
}

  • 1 Creates an interval, and saves it as a property of the vm, so you can use a reference in the future
  • 2 Increments the percent

Now the progress bar will increment over time! There’s still a final test to add to make sure the timer is removed when the progress bar stops running.

You might be wondering why you saved a reference to the setInterval call in the component. This is so that you can stop the interval running when the finish method is called, by calling its counter method clearInterval. To test that clearInterval is called, you’ll learn how to use spies.

4.2.2. Testing using spies

When a government wants to find secret information, it sends in its spies. When developers want to find secret information about functions, we use our own spies!

Often when you test code that uses an API that you don’t control, you need to check that a function in the API has been called. For example, suppose you were running code in a browser and wanted to test that window.clearInterval was called. How would you do that?

One way is to use spies. Lots of libraries implement spies, but because you’re using kitchen-sink Jest, you can create a spy with the jest.spyOn function. The spyOn function gives you the ability to check that a function was called using the toHaveBeenCalled matcher, as shown in the next listing.

Listing 4.11. Using a spy to test someMethod was called
jest.spyOn(window, 'someMethod')               1
window.someMethod ()
expect(window.someMethod).toHaveBeenCalled()   2

  • 1 Creates a spy
  • 2 Uses a toHaveBeenCalled matcher to test whether a spy was called

In your ProgressBar component, when the finish method is called, you should call clearInterval with the timer ID returned from the original setInterval call. This will clear the timer and stop potential memory leaks.

This presents two problems. First, how do you test that a function was called with an argument? Here, spies have your back. You can test that a spy was called with a specific argument by using the toHaveBeenCalledWith matcher as follows:

expect(window.someMethod).toHaveBeenCalledWith(123)

The next question is, how do you know what value clearInterval should be called with? To do that, you need to control the return value of setInterval. The fake timer functions can be configured to return a value using a mockReturnValue function, so you can configure setInterval to return any value you want, as follows:

setInterval.mockReturnValue(123)

In your test, you’ll configure setInterval to return a value. Then you’ll spy on clearInterval, call the finish method, and check that clearInterval was called with the value returned by setInterval. Copy the code from the next listing into src/components/__tests__/ProgressBar.spec.js.

Listing 4.12. Using jest.spyOn to test clearInterval
test('clears timer when finish is called', () => {
  jest.spyOn(window, 'clearInterval')                     1
  setInterval.mockReturnValue(123)                        2
  const wrapper = shallowMount(ProgressBar)
  wrapper.vm.start()                                      3
  wrapper.vm.finish()
  expect(window.clearInterval).toHaveBeenCalledWith(123)  4
})

  • 1 Spies on the clearInterval function
  • 2 Configures setInterval to return 123
  • 3 Calls start to start the timer
  • 4 Asserts that the clearInterval mock was called with the value returned from setInterval

This kind of test makes me uncomfortable. You had to use methods to control how functions behave—which means you’ve made assumptions about how a function works. The more assumptions in a test, the more chance there is of one being wrong and the test passing even though the production code is failing. Sometimes there’s no other way to test an external method except by making assumptions, but you should feel slightly guilty each time you do it. In your tests, keep assumptions to a minimum.

Now make sure the test fails with an assertion error: npm run test:unit. You can make the test pass by calling clearInterval with the timer ID in the finish method. Open src/components/ProgressBar.vue, and add the code from the next listing to the finish method in ProgressBar.

Listing 4.13. ProgressBar.vue
finish () {
  this.percent = 100
  this.hidden = true
  clearInterval(this.timer)      1
}

  • 1 Clears the setInterval timeout with the timer ID saved to this.timer

Run the tests: npm run test:unit. Great—the ProgressBar component is finished. Now you can set it up so that other components in your applications can run the progress bar by calling the start and finish methods. You can do that by adding a mounted ProgressBar as a Vue instance property.

4.3. Adding properties to the Vue instance

A common pattern in Vue is to add properties to the Vue base constructor. When a property is added to the Vue constructor, every child instance has access to those properties.

You can add Vue instance properties by adding a property to the Vue constructor prototype as shown in the next listing.

Definitions

An object’s prototype property is used to implement inheritance in JavaScript. Prototypal inheritance is a big topic—too big for me to teach in this book. If you want to learn about prototype-based inheritance, you can read the following guide on MDN—http://mng.bz/1daY.

Listing 4.14. Adding an instance property to the Vue prototype
Vue.prototype.$instanceProperty = 'hello'     1

const ChildComponent = {                      2
  template: '<p>{{$instanceProperty}}</p>'
}
new Vue({
  el: '#app',
  render: h => ChildComponent
})

  • 1 Adds a property to the Vue constructor prototype
  • 2 A ChildComponent that will have access to $instanceProperty

You’re going to use a clever technique and add a mounted ProgressBar component instance as a $bar instance property. That way, each component in the app can start the ProgressBar by calling $bar.start and stop the ProgressBar by calling $bar.finish.

Tip

In Vue, it’s a convention to give methods added to the prototype a dollar sign ($) prefix. This is to avoid possible naming collisions with local instance state values.

While you’re in main.js you should remove the data-fetching logic, which you’ll reimplement in the ItemList component. Replace the code in src/main.js with the following code.

Listing 4.15. Adding a Vue instance to the prototype
import Vue from 'vue'
import App from './App'
import ProgressBar from './components/ProgressBar'

Vue.config.productionTip = false

const bar = new Vue(ProgressBar).$mount()    1
Vue.prototype.$bar = bar                     2
document.body.appendChild(bar.$el)           3

new Vue({                                    4
  el: '#app',
  render: h => h(App)
})

  • 1 Creates a mounted ProgressBar instance
  • 2 Adds the mounted progress bar to the base Vue constructor prototype, which will be available to child component instances
  • 3 Adds the ProgressBar root element to the Document <body>
  • 4 Creates a new Vue instance using #app as the root element

With the new code in main.js, you’re creating a separate Vue instance of the ProgressBar component and adding it to the Vue base constructor prototype. Now you can write the code and tests for other components in the app that will call the ProgressBar methods.

This is a good time to demonstrate the downside of unit tests. Run the unit tests in the command line with npm run test:unit. They all pass. The thing is, your app is now completely broken!

If you run the dev server, you’ll see that the app doesn’t render any items. This is a problem with unit tests. Although unit tests tell you that units work in isolation, you don’t know that they work when they are connected in production. By the end of the book you’ll have a test suite that doesn’t suffer from this problem by supplementing the unit tests with end-to-end tests, but for now just know that you can’t rely on unit tests alone!

Let’s move on. The plan is to rewrite the ItemList component to fetch data and set the progress bar running while the data is fetched. To write tests for this functionality, you need to learn how to test with mocks.

4.4. Mocking code

Production code can be messy. It can make HTTP calls, open database connections, and have complex dependency trees. In unit tests, you can ignore all of that nonsense by mocking code.

In simple terms, mocking code is replacing code you don’t control with code you do control. Three benefits to mocking code follow:

1.  You can stop side effects like HTTP calls in tests.

2.  You can control how a function behaves, and what it returns.

3.  You can test that a function was called.

Earlier, you wrote tests for code that used timer functions. Instead of using the native timer functions, you used Jest to replace them with mock functions you could control. In other words, you used Jest to mock the timer functions.

In this section you’ll learn how to mock code in tests by refactoring the ItemList component to fetch the Hacker News data and run the progress bar. The first test that you’ll write will check that ItemList calls the instance property $bar start method. To write that test, you need to learn how to mock Vue instance properties.

4.4.1. Mocking Vue instance properties in components

It’s a common pattern in Vue to add properties to the Vue prototype. If a component uses an instance property, the instance property becomes a dependency of the component.

In your application, you have a mounted ProgressBar as a $bar instance property that will be available to all component instances. This works because under the hood all your component instances are created using the Vue base constructor.

In your tests, however, you mount components directly. The main.js entry file isn’t run, so $bar is never added as an instance property (figure 4.2). I call this the leaky bucket problem. When your component uses Vue instance properties, it’s like a bucket with holes in it. If you mount the component in your tests without patching the holes by adding the required properties, the bucket will leak, and you’ll get errors.

Figure 4.2. Injecting a property into the Vue instance tree

To solve the leaky bucket problem, you need to add properties to the Vue instance before you mount the component in a test. You can do that with the Vue Test Utils mocks option shown next.

Listing 4.16. Injecting an instance property with the mocks option
shallowMount(ItemList, {
  mocks: {
    $bar: {
      start: () => {}
    }
  }
})

The mocks option makes it easy to control instance properties. You just need to make sure you use it to patch the holes before you run the test.

Now that you know how to mock $bar in your tests, the next thing to do is figure out how to test that the $bar.start function was called. One way to do that is with a Jest mock function.

4.4.2. Understanding Jest mock functions

Sometimes in tests you need to check that a function was called. You can do that by replacing the function with a mock function that records information about itself.

Let’s look at a simple implementation of a mock function. The mock function has a calls array to store details on the function calls. Each time the function is called, it pushes the arguments it was called with to the calls array, as shown in the next listing.

Listing 4.17. Storing function calls
const mock = function(...args) {
  mock.calls.push(args)           1
}
mock.calls = []                   2
mock(1)
mock(2,3)
mock.calls // [[1], [1,2]]        3

  • 1 Pushes arguments to the calls array
  • 2 Initializes the calls array
  • 3 Calls are stored in an array

You can add lots of cool features to mock functions, but it doesn’t make sense to write your own mock functions when other solutions exist. Kitchen-sink Jest includes a mock function implementation. You can create a mock function by calling jest.fn, shown in the next code sample.

Listing 4.18. Using a Jest mock function
const mockFunction = jest.fn()            1
mockFunction(1)
mock(2,3)
mockFunction.mock.calls // [[1], [1,2]]   2

  • 1 Creates a mock function
  • 2 Accesses the function calls

You can combine Jest mock functions with Jest matchers to write expressive tests as follows:

expect(mockFunction).toHaveBeenCalled()

You probably recognize that matcher. You used it before to test that the spied clearInterval function was called. Under the hood, jest.spyOn and jest.useFakeTimers use Jest mock functions. jest.fn is just another interface for creating a Jest mock function.

Armed with Jest mock functions, you’re ready to write some tests. You can check that the ItemList component sets the progress bar running when it mounts by mounting ItemList with a $bar object using a mock function, as follows:

const $bar = {
  start: jest.fn()
}

Then you can use the Jest toHaveBeenCalledTimes matcher to assert that start was called. Add the code from the next listing to the describe block in src/views/__tests__/ItemList.spec.js.

Listing 4.19. Stubbing a function with a Jest mock
test('calls $bar start on load', () => {
  const $bar = {                                1
    start: jest.fn(),                           2
    finish: () => {}
  }
  shallowMount(ItemList, {mocks: { $bar }})     3
  expect($bar.start).toHaveBeenCalledTimes(1)   4
})

  • 1 Creates a fake $bar object
  • 2 Creates a jest mock using the jest.fn method
  • 3 Makes $bar available as this.$bar in ItemList
  • 4 Uses the toHaveBeenCalledTimes matcher to check that $bar.start was called

The test will fail with a nice assertion error when you run npm run test:unit. To call start when the component is mounted and pass the test, you need to learn about Vue lifecycle hooks.

4.4.3. Using Vue lifecycle hooks

Vue lifecycle hooks are built-in, optional functions that run at points during a component’s lifecycle. Lifecycle hooks are like instructions. When a component runs, it looks for the instructions for what to do at certain stages; if they exist, it executes them.

Note

You can see a detailed diagram of all the lifecycle hooks on the Vue website at http://mng.bz/wE2P.

To make sure the ItemList component starts the progress bar when it’s mounted, you’ll use the beforeMount hook. This hook runs—you guessed it—before the component mounts.

In your app, you have a failing test that you added in the previous section. It tests that ItemList calls $bar.start when the component is mounted. You can make that pass by adding a beforeMount hook.

Open src/views/ItemList.vue and add a beforeMount function that calls $bar.start in the component options object. You can see an example in the following listing.

Listing 4.20. Using the beforeMount lifecycle event
beforeMount () {
  this.$bar.start()
}

Run the tests with npm run test:unit. Hmm, the test now passes, but a previous test is failing! It’s the leaky bucket problem—the previous test is trying to call $bar.start when it doesn’t exist. The solution is to pass in a $bar object with a start method in the broken test. In the next section you’ll update this broken test to change how it receives data, so you can fix the broken test then.

Now when your app starts, the progress bar will start. The purpose of this is to make it visible to the users that the application is loading data. The next step is to actually load the Hacker News data. You’ll do this in the ItemList component. Remember that you get the Hacker News data by calling functions in an API file. To test that you’re calling API functions in the ItemList component, you need to learn how to mock imported module dependencies.

4.5. Mocking module dependencies

Trying to isolate a unit to test can be like removing a plant from the ground. You pull the plant out, only to discover the roots are tangled around other plants. Before you know it, you have pulled up half the garden.

When a JavaScript file imports another module, the imported module becomes a module dependency. Most of the time, it’s fine to have module dependencies in a unit test; but if the module dependency has side effects, like making a HTTP request, it can cause problems.

Mocking module dependencies is the process of replacing an imported module with another object. It’s a useful tool to have in your testing tool belt.

In your code, you’re going to fetch data in the ItemList component. ItemList will make a call to the fetchListData function exported by src/api/api.js. The fetch-ListData function makes a request to an external Hacker News API (figure 4.3).

Figure 4.3. Importing a function from another file

HTTP requests don’t belong in unit tests. They slow down your unit tests, and they stop unit tests from being reliable (HTTP requests are never 100% reliable). You need to mock the api.js file in your unit test so that fetchListData never makes an HTTP request (figure 4.4).

Figure 4.4. Stubbing a file import

You can stub a file in a couple of ways. One way is to use Jest spies, as shown in the next listing.

Listing 4.21. Mocking a module dependency
import * as api from '../../api/api'                                 1

jest.spyOn(api, 'fetchListData')                                     2

api.fetchListData.mockImplementation(() => Promise.resolve([])) //   3

  • 1 Imports the exports from api on an api object. You need to import the functions as an object in order to use jest.spyOn.
  • 2 Replaces fetchListData in the Jest mock function
  • 3 Changes the implementation of fetchListData

This works fine, but according to the JavaScript spec, the code in listing 4.21 is not valid. With ES modules, you cannot reassign imported module values. Because of this, you need to find another way.

Luckily, there’s an alternative. Jest has its own module resolver, which you can configure to return files that you want.

Note

A module resolver is a function that finds a file and makes it available to another file. Usually in JavaScript you use the node module resolver. You can read about the node module system in the node docs—http://mng.bz/qB1r.

Let’s use the Jest mock system to mock module dependencies and help write tests.

4.5.1. Using Jest mocks to mock module dependencies

Jest provides an API to choose which files or functions are returned when one module imports another module. To use this feature, you need to create a mock file that Jest should resolve with, instead of the requested file. The mock file will contain functions that you want the test to use instead of the real file functions.

Imagine you wanted to mock a file called http-service.js that exports a fetchData function, as follows:

export function fetchData() {
  return fetch('https://example.com/data')
}

fetch makes an HTTP request, which you don’t want. Instead, you can create a mock file that exports a mock fetchData function, as follows:

export const fetchData = jest.fn()

You create a mock file by adding a file to a __mocks__ directory with the same name as the file you want to mock. For example, to mock the api.js file you would create a src/api/__mocks__/api.js file that exports a fetchListData mock function.

You tell Jest to mock a file by calling the jest.mock function as follows:

jest.mock('./src/api.js')

After this function is called, when a module imports src/api/api.js, Jest will resolve using the mock file you created instead of the original file.

It’s time to create your own mock file. Add a __mocks__ directory in the src/api directory, and create a file named api.js (full path: src/api/__mocks__/api.js). In the file, you’ll export a fetchListData mock function. The mock function should return a promise that resolves with an array, because the real fetchListData function returns a promise with an array of items.

By default, Jest mock functions created with jest.fn are no-operation functions—they don’t do anything. You can set the implementation of a mock function by calling jest.fn with the intended function implementation. For example, you could create a mock function that always returned true as follows:

jest.fn(() => true)

Add the mock fetchListData function from the next listing into src/api/__mocks__/api.js.

Listing 4.22. Creating a mock file
export const fetchListData = jest.fn(() => Promise.resolve([]))     1

  • 1 Sets fetchListData as the Jest mock function that returns a resolved promise

Now you have a mock file that you can use to write tests for code that calls the fetchListData function from the api file. These tests are going to be asynchronous, because even when they are mocked, promises still run asynchronously, so you need to learn to test asynchronous code.

4.5.2. Testing asynchronous code

Asynchronous code requires some careful testing. I’ve seen it bite people before, and it will bite people again. Luckily, though, if you’re working with promises or async/await functions, writing asynchronous tests is easy!

Definition

async/await functions can be used to write asynchronous code in a way that looks synchronous. If you’re unfamiliar with async/await, you can read about them at this blog post—https://javascript.info/async-await.

Imagine you’re testing a fetchData function that returns a promise. In the test, you need to test the resolved data returned by fetchData. If you use async/await, you can just set the test function to be an asynchronous function, tell Jest to expect one assertion, and use await in the test, as shown in listing 4.23.

Note

The reason you should set the number of assertions in an asynchronous test is to make sure all the assertions execute before the test finishes.

Listing 4.23. Writing an asynchronous test
test('fetches data', async () => {        1
  expect.assertions(1)                    2
  const data = await fetchListData()      3
  expect(data).toBe('some data')
})

  • 1 Uses async as a test function
  • 2 Sets the number of assertions the test should run, so that the test fails if a promise is rejected
  • 3 Waits until the async function finishes executing
Note

If the function you are testing uses callbacks, you will need to use the done callback. You can read how to do this in the Jest docs—http://mng.bz/7eYv.

But when you’re testing components that call asynchronous code, you don’t always have access to the asynchronous function you need to wait for. That means you can’t use await in the test to wait until the asynchronous function has finished. This is a problem, because even when a function returns a resolved promise, the then callback does not run synchronously, as shown in the next listing.

Listing 4.24. Testing a promise
test('awaits promise', async () => {
  expect.assertions(1)
  let hasResolved = false
  Promise.resolve().then(() => {    1
    hasResolved = true
  })

  expect(hasResolved).toBe(true)    2
})

  • 1 Resolved promise that sets hasResolved to true in the then callback
  • 2 hasResolved is still false, because the then callback has not run, so the assertion fails.

But fear not, you can wait for fulfilled then callbacks to run by using the flush-promises library, shown next.

Listing 4.25. Flushing promises
test('awaits promises', async () => {
  expect.assertions(1)
  let hasResolved = false
  Promise.resolve().then(() => {       1
    hasResolved = true
  })
  await flushPromises()                2

  expect(hasResolved).toBe(true)
})

  • 1 Resolved promise that sets hasResolved to true in the then callback
  • 2 Waits until all pending promise callbacks have run. If you remove this line the test will fail, because the code inside hasResolved will not run before the test finishes.
Note

If you want to know how flush-promises works, you need to understand the difference between the microtask queue and the task queue. It’s quite technical stuff and definitely not required for this book. If you’re interested you can read this excellent post by Jake Archibald to get you started—https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules.

You’ll use flush-promises throughout this book to wait for promises in asynchronous tests, so you need to install it as a development dependency. Enter the following command in the command line:

npm install --save-dev flush-promises

After that overview of asynchronous testing and module dependency mocking, you’re ready to use the skills to write asynchronous tests. In case you’ve forgotten, you’re going to move the data-fetching logic into the ItemList component. Before you add new tests, you will refactor the existing tests to use fetchListData, instead of setting data on window.items.

The first thing you need to do is tell Jest to use the mock api file you created. Add the following code to the top of the file in src/views/__tests__ /ItemList.spec.js.

Listing 4.26. Mocking a module dependency with Jest
jest.mock('../../api/api.js')

You need to import flush-promises to wait for pending promises. You also need to import the mock fetchListData function to configure what it returns. Add the following code below the existing import declarations in src/views/__tests__/ItemList .spec.js:

import flushPromises from 'flush-promises'
import { fetchListData } from '../../api/api'

Now you can refactor the existing test to use fetchListData. Replace the existing renders an Item with data for each item in window.items test with the code in the following listing.

Listing 4.27. Stubbing a module dependency in tests
test('renders an Item with data for each item', async () => {
  expect.assertions(4)                                         1
  const $bar = {                                               2
    start: () => {},
    finish: () => {}
  }
  const items = [{ id: 1 }, { id: 2 }, { id: 3 }]
  fetchListData.mockResolvedValueOnce(items)                   3
  const wrapper = shallowMount(ItemList, {mocks: {$bar}})
  await flushPromises()
  const Items = wrapper.findAll(Item)
  expect(Items).toHaveLength(items.length)                     4
  Items.wrappers.forEach((wrapper, i) => {
    expect(wrapper.vm.item).toBe(items[i])
  })
})

  • 1 Defines four assertions so that the test fails if a promise is rejected
  • 2 Adds a $bar mock with finish and start functions, so that this test does not error when you use the finish function in a future test
  • 3 Configures fetchListData to resolve with the items array
  • 4 Waits for promise callbacks to run

Now the test will fail with an assertion error. Before you make the test pass, you’ll add new tests to make sure the correct progress bar methods are called when the data is loaded successfully and when the data loading fails.

The first test will check that $bar.finish is called when the data resolves, using the same flush-promises technique. You don’t need to mock the implementation of fetchListData, because you set it resolve with an empty array in the mock file.

Add the test from the next listing to the describe block in src/views/__tests__/ItemList.spec.js.

Listing 4.28. Using flush-promises in a test
test('calls $bar.finish when load is successful', async () => {
  expect.assertions(1)
  const $bar = {
    start: () => {},
    finish: jest.fn()
  }
  shallowMount(ItemList, {mocks: {$bar}})
  await flushPromises()                         1

  expect($bar.finish).toHaveBeenCalled()        2
})

  • 1 Waits for pending promise callbacks
  • 2 Asserts that the mock was called

Run the tests with npm run test:unit. Make sure you see an assertion error. Asynchronous tests are the biggest cause of false positives. Without the Jest expect.assertions call, if an assertion is inside an asynchronous action but the test doesn’t know that it’s asynchronous, the test will pass because it never executes the assertion.

After you’ve seen an assertion error, you can update ItemList to make the tests pass. Open src/views/ItemList.vue, and replace the <script> block with the code in the next listing.

Listing 4.29. ItemList.vue
<script>
import Item from '../components/Item.vue'
   import { fetchListData } from '../api/api'    1

export default {
components: {
    Item
  },
  beforeMount () {
    this.loadItems()                             2
  },
  data () {
    return {
      displayItems: []                           3
    }
  },
  methods: {
    loadItems () {                               4
      this.$bar.start()                          5
      fetchListData('top')                       6
      .then(items => {
        this.displayItems = items                7
        this.$bar.finish()
      })
    }
  }
}
</script>

  • 1 Imports methods from the api file
  • 2 Calls the loadItems method before the component is mounted
  • 3 Sets the default displayItems to an empty array
  • 4 Declares the loadItems function
  • 5 Calls the ProgressBar start method to start the bar running
  • 6 Fetches items for the Hacker News top list
  • 7 Sets the component displayItems to the returned Items

Phew, that was some heavy refactoring. Make sure the tests pass with npm run test :unit.

The final test of this chapter will check that $bar.fail is called when the fetchListData function is unsuccessful (even though it’s not yet implemented in the ProgressBar component!). You can test this by mocking fetchListData to return a rejected promise. Add the code from the following listing to the describe block in src/views/__tests__/ItemList.spec.js.

Listing 4.30. Mocking function to reject
test('calls $bar.fail when load unsuccessful', async () => {
  expect.assertions(1)
  const $bar = {
    start: () => {},
    fail: jest.fn()
  }
  fetchListData. mockRejectedValueOnce()    1
  shallowMount(ItemList, {mocks: {$bar}})
  await flushPromises()

  expect($bar.fail).toHaveBeenCalled()      2
})

  • 1 Rejects when fetchListData is called, so you can test an error case
  • 2 Asserts that the mock was called

If you run the tests, you’ll see that the test fails with an error message that no assertions were called. This is because you used expect.assertions(1) to tell Jest that there should be one assertion. When the fetchListData returned a promise, it caused the test to throw an error, and the assertion was never called. This is why you should always define how many assertions you expect in asynchronous tests.

You can make the test pass by adding a catch handler to the fetchListData call in ItemList. Open src/views/ItemList.vue, and add update the loadItems method to include a catch handler as follows:

loadItems () {
  this.$bar.start()
  fetchListData ('top')
  .then(items => {
    this.displayItems = items
    this.$bar.finish()
  })
  .catch(() => this.$bar.fail())
}

Congratulations, you’ve moved the data-fetching logic to the ItemList component! Before you go off into the world with your new mocking knowledge like a driver who has just gotten their license, I need to talk to you about using mocks responsibly.

4.5.3. Using mocks in moderation

With great power, comes great responsibility. Mocking is a testing superpower, but you need to use mocks carefully.

You’ve learned how to use mocks in different ways: to control the return of a function, to check that a function was called, and to stop side effects like HTTP requests. These are all great use cases for mocking, because they are difficult to test without mocks. But mocking should always be a last resort.

Mocking increases the coupling between a test and production code, and it increases the assumptions your test makes. Each extra mock in a test is new opportunity for your test code and production code to go out of sync.

Mocking module dependencies is the most heavy-handed form of mocking. You should mock only files with side effects that will slow down tests. Some common side effects that slow down tests follow:

  • HTTP calls
  • Connecting to a database
  • Using the filesystem

I’m not telling you not to use mocks. I just want to warn you that they are potentially dangerous and cover my back so you can’t get angry at me if you use too many mocks in your future tests!

In the next chapter you’ll learn how to test events in Vue. You’ll build on the techniques you learned in this chapter, including using mocks, to test an interactive email sign-up form.

Summary

  • Public methods without dependencies are simple to test, by calling the component and asserting the component output.
  • You can test code that uses timer functions by mocking the timer functions with Jest fake timers.
  • You can test that dependencies are called by using Jest spies and Jest mock functions.
  • You can use mock functions to control the return value of a dependency.
  • You can mock Vue instance properties using the Vue Test Utils mocks option.
  • You can use Jest mock methods to change how a Jest mock function behaves.
  • You can mock module dependencies with jest.mock.

Exercises

1

In this chapter you tested finish and start methods in the ProgressBar component, but you didn’t write a fail method. Can you write a test to check that the ProgressBar root element has an error class added to it after fail is called? Add the test code to src/components/__tests__/ProgressBar.spec.js.

2

Can you write a test to check that the ProgressBar root element has a width style property of 100% after fail is called? Add the test code to src/components/__tests__/ProgressBar.spec.js.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.162.110