Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add async guide #282

Merged
merged 14 commits into from
Dec 26, 2017
159 changes: 159 additions & 0 deletions docs/en/guides/testing-async-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Testing Asynchronous Components
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This headline might be confused with "Async components".

Maybe: "Testing Asynchronous Behaviour" ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch @LinusBorg . I think your suggested title is better. Minor, but In my day to day life I spell it behaviour, should use use American English (no u) or English/Europe/everywhere else English?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm. I think we should probably stick do American english as that's the bigger "market"?

OTOH @eddyerburgh is british so ..., :-P

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed I am British 🇬🇧

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am Australian (English colony) so we use the same style. But I agree we should use the American English since I believe the official docs would use that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we should use US English


> An example project will be made available.

To simplify testing, `vue-test-utils` applies updates _synchronously_. However, there are some techniques you need to be aware of when testing a component with asynchronous behavior such as callbacks or promises.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applies updates synchronously

to

applies DOM updates synchronously


One common cases is components that use `watch`, which updates asynchronously. Below in an example of a component that renders some content based on a boolean value, which is updated using a watcher:

``` js
<template>
<div>
<input v-model="text" />
<button v-if="showButton">Submit</button>
</div>
</template>

<script>
export default {
data () {
return {
text: '',
showButton: false
}
},

watch: {
text () {
this.showButton = true
}
}
}
</script>
```

This component conditionally renders a submit button, based on whether the user entered some text. Let's see how we might test this:

``` js
import { shallow } from 'vue-test-utils'
import Foo from './Foo'

describe('Foo', () => {
it('renders a button async when the user enters text', () => {
expect.assertions(2)
const wrapper = shallow(Foo)

wrapper.find('input').element.value = 'Value'
wrapper.find('input').trigger('input')

expect(wrapper.vm.showButton).toBe(true)
expect(wrapper.find('button').exists()).toBe(true)
})
})
```

The above test fails - however, when you test it by hand in a browser, it seems fine! The reason the test is failing is because properties inside of `watch` are updated asynchronously. In this test, the assertion occurs before `nextTick()` is called, so the `button` is still not visible.

Most unit test libraries provide a callback to let the runner know when the test is complete. Jest and Karma both use `done()`. We can use this, in combination with `nextTick()`, to test the component.


``` js
import { shallow } from 'vue-test-utils'
import Foo from './Foo'

describe('Foo', () => {
it('renders a button async when the user enters text', (done) => {
expect.assertions(2)
const wrapper = mount(Foo)

wrapper.find('input').element.value = 'Value'
wrapper.find('input').trigger('input')

wrapper.vm.$nextTick(() => {
expect(wrapper.vm.showButton).toBe(true)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this assertion throws, will the error be caught, or will the test time out? In my experience, it's the latter.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind you, I've been using mocha which doesn't handle uncaught promise errors. might be different with jest

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will time out @callumacrae . What is the best thing to do in this situation?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure. i made an issue about it here: #213

expect(wrapper.find('button').exists()).toBe(true)
done()
})
})
})
```

Now the test passes!

Another common asynchronous behavior is API calls and Vuex actions. The following examples shows how to test a method that makes an API call. This example is using Jest to run the test and to mock the HTTP library `axios`.

The implementation of the `axios` mock looks like this:

``` js
export default {
get: () => new Promise(resolve => {
resolve({ data: 'value' })
})
}
```

The below component makes an API call when a button is clicked, then assigns the response to `value`.

``` js
<template>
<button @click="fetchResults" />
</template>

<script>
import axios from 'axios'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add a line break after the import statement

export default {
data () {
return {
value: null
}
},

methods: {
async fetchResults () {
const response = await axios.get('mock/service')
this.value = response.data
}
}
}
</script>
```
A test can be written like this:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a link to the Jest manual mocks page—https://facebook.github.io/jest/docs/en/manual-mocks.html#content


``` js
import { shallow } from 'vue-test-utils'
import Foo from './Foo'
jest.mock('axios')

test('Foo', () => {
it('fetches async when a button is clicked', () => {
const wrapper = shallow(Foo)
wrapper.find('button').trigger('click')
expect(wrapper.vm.value).toEqual('value')
})
})
```

This test currently fails, because the assertion is called before the promise resolves. One solution is to use the npm package, `flush-promises`. which immediately resolve any unresolved promises. This test is also asynchronous, so like the previous example, we need to let the test runner know to wait before making any assertions.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which immediately resolve any unresolved promises

to

which can be used to flush all pending resolved promise handlers


If you are using Jest, there are a few options, such as passing a `done()` callback, as shown above. Another is to prepend the test with the ES7 'async' keyword. We can now use the the ES7 `await` keyword with `flushPromises()`, to immediately resolve the API call.

The updated test looks like this:

``` js
import { shallow } from 'vue-test-utils'
import flushPromises from 'flush-promises'
import Foo from './Foo'
jest.mock('axios')

test('Foo', () => {
it('fetches async when a button is clicked', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to make the function async, that's why there's a linting error:
it('fetches async when a button is clicked', () => {

to

it('fetches async when a button is clicked', async () => {

const wrapper = shallow(Foo)
wrapper.find('button').trigger('click')
await flushPromises()
expect(wrapper.vm.value).toEqual('value')
})
})
```

This same technique can be applied to Vuex actions, which return a promise by default.