• Tidak ada hasil yang ditemukan

Testing asynchronous code

Dalam dokumen Testing Vue. Js Applications ( PDFDrive ) (Halaman 191-200)

About the cover illustration

3. You can test that a function was called

4.5. MOCKING MODULE DEPENDENCIES

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()

Dalam dokumen Testing Vue. Js Applications ( PDFDrive ) (Halaman 191-200)