Commit 22a434d7 authored by Lee Tickett's avatar Lee Tickett Committed by Marcia Ramos

Tweaking Vue Testing Docs

parent e208e4a8
...@@ -119,7 +119,8 @@ Please check this [rules](https://github.com/vuejs/eslint-plugin-vue#bulb-rules) ...@@ -119,7 +119,8 @@ Please check this [rules](https://github.com/vuejs/eslint-plugin-vue#bulb-rules)
## Naming ## Naming
1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension ([#34371](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/34371)). 1. **Extensions**: Use `.vue` extension for Vue components. Do not use `.js` as file extension
([#34371](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/34371)).
1. **Reference Naming**: Use PascalCase for their instances: 1. **Reference Naming**: Use PascalCase for their instances:
```javascript ```javascript
...@@ -402,7 +403,8 @@ When using `v-for` you need to provide a *unique* `:key` attribute for each item ...@@ -402,7 +403,8 @@ When using `v-for` you need to provide a *unique* `:key` attribute for each item
</div> </div>
``` ```
1. When using `v-for` with `template` and there is more than one child element, the `:key` values must be unique. It's advised to use `kebab-case` namespaces. 1. When using `v-for` with `template` and there is more than one child element, the `:key` values
must be unique. It's advised to use `kebab-case` namespaces.
```html ```html
<template v-for="(item, index) in items"> <template v-for="(item, index) in items">
...@@ -468,9 +470,10 @@ Useful links: ...@@ -468,9 +470,10 @@ Useful links:
## Vue testing ## Vue testing
Over time, a number of programming patterns and style preferences have emerged in our efforts to effectively test Vue components. Over time, a number of programming patterns and style preferences have emerged in our efforts to
The following guide describes some of these. **These are not strict guidelines**, but rather a collection of suggestions and effectively test Vue components. The following guide describes some of these.
good practices that aim to provide insight into how we write Vue tests at GitLab. **These are not strict guidelines**, but rather a collection of suggestions and good practices that
aim to provide insight into how we write Vue tests at GitLab.
### Mounting a component ### Mounting a component
...@@ -479,8 +482,10 @@ Typically, when testing a Vue component, the component should be "re-mounted" in ...@@ -479,8 +482,10 @@ Typically, when testing a Vue component, the component should be "re-mounted" in
To achieve this: To achieve this:
1. Create a mutable `wrapper` variable inside the top-level `describe` block. 1. Create a mutable `wrapper` variable inside the top-level `describe` block.
1. Mount the component using [`mount`](https://vue-test-utils.vuejs.org/api/#mount)/[`shallowMount`](https://vue-test-utils.vuejs.org/api/#shallowMount). 1. Mount the component using [`mount`](https://vue-test-utils.vuejs.org/api/#mount)/
1. Reassign the resulting [`Wrapper`](https://vue-test-utils.vuejs.org/api/wrapper/#wrapper) instance to our `wrapper` variable. [`shallowMount`](https://vue-test-utils.vuejs.org/api/#shallowMount).
1. Reassign the resulting [`Wrapper`](https://vue-test-utils.vuejs.org/api/wrapper/#wrapper)
instance to our `wrapper` variable.
Creating a global, mutable wrapper provides a number of advantages, including the ability to: Creating a global, mutable wrapper provides a number of advantages, including the ability to:
...@@ -497,14 +502,16 @@ Creating a global, mutable wrapper provides a number of advantages, including th ...@@ -497,14 +502,16 @@ Creating a global, mutable wrapper provides a number of advantages, including th
}) })
``` ```
- Use a `beforeEach` block to mount the component (see [the `createComponent` factory](#the-createcomponent-factory) for more information). - Use a `beforeEach` block to mount the component (see
[the `createComponent` factory](#the-createcomponent-factory) for more information).
- Use an `afterEach` block to destroy the component, for example, `wrapper.destroy()`. - Use an `afterEach` block to destroy the component, for example, `wrapper.destroy()`.
#### The `createComponent` factory #### The `createComponent` factory
To avoid duplicating our mounting logic, it's useful to define a `createComponent` factory function To avoid duplicating our mounting logic, it's useful to define a `createComponent` factory function
that we can reuse in each test block. This is a closure which should reassign our `wrapper` variable that we can reuse in each test block. This is a closure which should reassign our `wrapper` variable
to the result of [`mount`](https://vue-test-utils.vuejs.org/api/#mount) and [`shallowMount`](https://vue-test-utils.vuejs.org/api/#shallowMount): to the result of [`mount`](https://vue-test-utils.vuejs.org/api/#mount) and
[`shallowMount`](https://vue-test-utils.vuejs.org/api/#shallowMount):
```javascript ```javascript
import MyComponent from '~/path/to/my_component.vue'; import MyComponent from '~/path/to/my_component.vue';
...@@ -568,7 +575,8 @@ describe('MyComponent', () => { ...@@ -568,7 +575,8 @@ describe('MyComponent', () => {
1. Consider using a single (or a limited number of) object arguments over many arguments. 1. Consider using a single (or a limited number of) object arguments over many arguments.
Defining single parameters for common data like `props` is okay, Defining single parameters for common data like `props` is okay,
but keep in mind our [JavaScript style guide](javascript.md#limit-number-of-parameters) and stay within the parameter number limit: but keep in mind our [JavaScript style guide](javascript.md#limit-number-of-parameters) and
stay within the parameter number limit:
```javascript ```javascript
// bad // bad
...@@ -591,6 +599,19 @@ the mounting function (`mount` or `shallowMount`) to be used to mount the compon ...@@ -591,6 +599,19 @@ the mounting function (`mount` or `shallowMount`) to be used to mount the compon
function createComponent({ mountFn = shallowMount } = {}) { } function createComponent({ mountFn = shallowMount } = {}) { }
``` ```
1. Wrap calls to `mount` and `shallowMount` in `extendedWrapper`, this exposes `wrapper.findByTestId()`:
```javascript
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { SomeComponent } from 'components/some_component.vue';
let wrapper;
const createWrapper = () => { wrapper = extendedWrapper(shallowMount(SomeComponent)); };
const someButton = () => wrapper.findByTestId('someButtonTestId');
```
### Setting component state ### Setting component state
1. Avoid using [`setProps`](https://vue-test-utils.vuejs.org/api/wrapper/#setprops) to set 1. Avoid using [`setProps`](https://vue-test-utils.vuejs.org/api/wrapper/#setprops) to set
...@@ -609,12 +630,13 @@ component state wherever possible. Instead, set the component's ...@@ -609,12 +630,13 @@ component state wherever possible. Instead, set the component's
``` ```
The exception here is when you wish to test component reactivity in some way. The exception here is when you wish to test component reactivity in some way.
For example, you may want to test the output of a component when after a particular watcher has executed. For example, you may want to test the output of a component when after a particular watcher has
Using `setProps` to test such behavior is okay. executed. Using `setProps` to test such behavior is okay.
### Accessing component state ### Accessing component state
1. When accessing props or attributes, prefer the `wrapper.props('myProp')` syntax over `wrapper.props().myProp`: 1. When accessing props or attributes, prefer the `wrapper.props('myProp')` syntax over
`wrapper.props().myProp` or `wrapper.vm.myProp`:
```javascript ```javascript
// good // good
...@@ -626,7 +648,8 @@ component state wherever possible. Instead, set the component's ...@@ -626,7 +648,8 @@ component state wherever possible. Instead, set the component's
expect(wrapper.attributes('myAttr')).toBe(true); expect(wrapper.attributes('myAttr')).toBe(true);
``` ```
1. When asserting multiple props, check the deep equality of the `props()` object with [`toEqual`](https://jestjs.io/docs/en/expect#toequalvalue): 1. When asserting multiple props, check the deep equality of the `props()` object with
[`toEqual`](https://jestjs.io/docs/en/expect#toequalvalue):
```javascript ```javascript
// good // good
...@@ -642,8 +665,9 @@ component state wherever possible. Instead, set the component's ...@@ -642,8 +665,9 @@ component state wherever possible. Instead, set the component's
}); });
``` ```
1. If you are only interested in some of the props, you can use [`toMatchObject`](https://jestjs.io/docs/en/expect#tomatchobjectobject). 1. If you are only interested in some of the props, you can use
Prefer `toMatchObject` over [`expect.objectContaining`](https://jestjs.io/docs/en/expect#expectobjectcontainingobject): [`toMatchObject`](https://jestjs.io/docs/en/expect#tomatchobjectobject). Prefer `toMatchObject`
over [`expect.objectContaining`](https://jestjs.io/docs/en/expect#expectobjectcontainingobject):
```javascript ```javascript
// good // good
...@@ -664,12 +688,24 @@ Prefer `toMatchObject` over [`expect.objectContaining`](https://jestjs.io/docs/e ...@@ -664,12 +688,24 @@ Prefer `toMatchObject` over [`expect.objectContaining`](https://jestjs.io/docs/e
The goal of this accord is to make sure we are all on the same page. The goal of this accord is to make sure we are all on the same page.
1. When writing Vue, you may not use jQuery in your application. 1. When writing Vue, you may not use jQuery in your application.
1. If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your application to grab data attributes using `dataset`. You can do this without jQuery. 1. If you need to grab data from the DOM, you may query the DOM 1 time while bootstrapping your
application to grab data attributes using `dataset`. You can do this without jQuery.
1. You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html). 1. You may use a jQuery dependency in Vue.js following [this example from the docs](https://vuejs.org/v2/examples/select2.html).
1. If an outside jQuery Event needs to be listen to inside the Vue application, you may use jQuery event listeners. 1. If an outside jQuery Event needs to be listen to inside the Vue application, you may use
1. We avoid adding new jQuery events when they are not required. Instead of adding new jQuery events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit). jQuery event listeners.
1. You may query the `window` object one time, while bootstrapping your application for application specific data (e.g. `scrollTo` is ok to access anytime). Do this access during the bootstrapping of your application. 1. We avoid adding new jQuery events when they are not required. Instead of adding new jQuery
1. You may have a temporary but immediate need to create technical debt by writing code that does not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in the first place. An issue should be created for that tech debt to evaluate it further and discuss. In the coming months you should fix that tech debt, with its priority to be determined by maintainers. events take a look at [different methods to do the same task](https://vuejs.org/v2/api/#vm-emit).
1. When creating tech debt you must write the tests for that code before hand and those tests may not be rewritten. e.g. jQuery tests rewritten to Vue tests. 1. You may query the `window` object one time, while bootstrapping your application for application
1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you must use the *store pattern* which can be found in the [Vue.js documentation](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch). specific data (for example, `scrollTo` is ok to access anytime). Do this access during the
1. Once you have chosen a centralized state-management solution you must use it for your entire application. i.e. Don't mix and match your state-management solutions. bootstrapping of your application.
1. You may have a temporary but immediate need to create technical debt by writing code that does
not follow our standards, to be refactored later. Maintainers need to be ok with the tech debt in
the first place. An issue should be created for that tech debt to evaluate it further and discuss.
In the coming months you should fix that tech debt, with its priority to be determined by maintainers.
1. When creating tech debt you must write the tests for that code before hand and those tests may
not be rewritten. For example, jQuery tests rewritten to Vue tests.
1. You may choose to use VueX as a centralized state management. If you choose not to use VueX, you
must use the *store pattern* which can be found in the
[Vue.js documentation](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch).
1. Once you have chosen a centralized state-management solution you must use it for your entire
application. Don't mix and match your state-management solutions.
...@@ -22,7 +22,8 @@ All new features built with Vue.js must follow a [Flux architecture](https://fac ...@@ -22,7 +22,8 @@ All new features built with Vue.js must follow a [Flux architecture](https://fac
The main goal we are trying to achieve is to have only one data flow and only one data entry. The main goal we are trying to achieve is to have only one data flow and only one data entry.
In order to achieve this goal we use [vuex](#vuex). In order to achieve this goal we use [vuex](#vuex).
You can also read about this architecture in Vue docs about [state management](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch) You can also read about this architecture in Vue docs about
[state management](https://vuejs.org/v2/guide/state-management.html#Simple-State-Management-from-Scratch)
and about [one way data flow](https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow). and about [one way data flow](https://vuejs.org/v2/guide/components.html#One-Way-Data-Flow).
### Components and Store ### Components and Store
...@@ -62,14 +63,15 @@ Be sure to read about [page-specific JavaScript](performance.md#page-specific-ja ...@@ -62,14 +63,15 @@ Be sure to read about [page-specific JavaScript](performance.md#page-specific-ja
While mounting a Vue application, you might need to provide data from Rails to JavaScript. While mounting a Vue application, you might need to provide data from Rails to JavaScript.
To do that, you can use the `data` attributes in the HTML element and query them while mounting the application. To do that, you can use the `data` attributes in the HTML element and query them while mounting the application.
You should only do this while initializing the application, because the mounted element is replaced with a Vue-generated DOM. You should only do this while initializing the application, because the mounted element is replaced
with a Vue-generated DOM.
The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function The advantage of providing data from the DOM to the Vue instance through `props` in the `render`
instead of querying the DOM inside the main Vue component is avoiding the need to create a fixture or an HTML element in the unit test, function instead of querying the DOM inside the main Vue component is avoiding the need to create a
which makes the tests easier. fixture or an HTML element in the unit test, which makes the tests easier.
See the following example, also, please refer to our [Vue style guide](style/vue.md#basic-rules) for additional See the following example, also, please refer to our [Vue style guide](style/vue.md#basic-rules) for
information on why we explicitly declare the data being passed into the Vue app; additional information on why we explicitly declare the data being passed into the Vue app;
```javascript ```javascript
// haml // haml
...@@ -94,13 +96,15 @@ return new Vue({ ...@@ -94,13 +96,15 @@ return new Vue({
}); });
``` ```
> When adding an `id` attribute to mount a Vue application, please make sure this `id` is unique across the codebase > When adding an `id` attribute to mount a Vue application, please make sure this `id` is unique
across the codebase.
#### Accessing the `gl` object #### Accessing the `gl` object
When we need to query the `gl` object for data that doesn't change during the application's life cycle, we should do it in the same place where we query the DOM. When we need to query the `gl` object for data that doesn't change during the application's life
By following this practice, we can avoid the need to mock the `gl` object, which makes tests easier. cycle, we should do it in the same place where we query the DOM. By following this practice, we can
It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component: avoid the need to mock the `gl` object, which makes tests easier. It should be done while
initializing our Vue instance, and the data should be provided as `props` to the main component:
```javascript ```javascript
return new Vue({ return new Vue({
...@@ -192,13 +196,18 @@ Check this [page](vuex.md) for more details. ...@@ -192,13 +196,18 @@ Check this [page](vuex.md) for more details.
In the [Vue documentation](https://vuejs.org/v2/api/#Options-Data) the Data function/object is defined as follows: In the [Vue documentation](https://vuejs.org/v2/api/#Options-Data) the Data function/object is defined as follows:
> The data object for the Vue instance. Vue recursively converts its properties into getter/setters to make it “reactive”. The object must be plain: native objects such as browser API objects and prototype properties are ignored. A rule of thumb is that data should just be data - it is not recommended to observe objects with their own stateful behavior. > The data object for the Vue instance. Vue recursively converts its properties into getter/setters
to make it “reactive”. The object must be plain: native objects such as browser API objects and
prototype properties are ignored. A rule of thumb is that data should just be data - it is not
recommended to observe objects with their own stateful behavior.
Based on the Vue guidance: Based on the Vue guidance:
- **Do not** use or create a JavaScript class in your [data function](https://vuejs.org/v2/api/#data), such as `user: new User()`. - **Do not** use or create a JavaScript class in your [data function](https://vuejs.org/v2/api/#data),
such as `user: new User()`.
- **Do not** add new JavaScript class implementations. - **Do not** add new JavaScript class implementations.
- **Do** use [GraphQL](../api_graphql_styleguide.md), [Vuex](vuex.md) or a set of components if cannot use simple primitives or objects. - **Do** use [GraphQL](../api_graphql_styleguide.md), [Vuex](vuex.md) or a set of components if
cannot use simple primitives or objects.
- **Do** maintain existing implementations using such approaches. - **Do** maintain existing implementations using such approaches.
- **Do** Migrate components to a pure object model when there are substantial changes to it. - **Do** Migrate components to a pure object model when there are substantial changes to it.
- **Do** add business logic to helpers or utils, so you can test them separately from your component. - **Do** add business logic to helpers or utils, so you can test them separately from your component.
...@@ -209,7 +218,8 @@ There are additional reasons why having a JavaScript class presents maintainabil ...@@ -209,7 +218,8 @@ There are additional reasons why having a JavaScript class presents maintainabil
- Once a class is created, it is easy to extend it in a way that can infringe Vue reactivity and best practices. - Once a class is created, it is easy to extend it in a way that can infringe Vue reactivity and best practices.
- A class adds a layer of abstraction, which makes the component API and its inner workings less clear. - A class adds a layer of abstraction, which makes the component API and its inner workings less clear.
- It makes it harder to test. Since the class is instantiated by the component data function, it is harder to 'manage' component and class separately. - It makes it harder to test. Since the class is instantiated by the component data function, it is
harder to 'manage' component and class separately.
- Adding OOP to a functional codebase adds yet another way of writing code, reducing consistency and clarity. - Adding OOP to a functional codebase adds yet another way of writing code, reducing consistency and clarity.
## Style guide ## Style guide
...@@ -231,6 +241,7 @@ Here's an example of a well structured unit test for [this Vue component](#appen ...@@ -231,6 +241,7 @@ Here's an example of a well structured unit test for [this Vue component](#appen
```javascript ```javascript
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -263,19 +274,21 @@ describe('~/todos/app.vue', () => { ...@@ -263,19 +274,21 @@ describe('~/todos/app.vue', () => {
}); });
// It is very helpful to separate setting up the component from // It is very helpful to separate setting up the component from
// its collaborators (i.e. Vuex, axios, etc.) // its collaborators (for example, Vuex and axios).
const createWrapper = (props = {}) => { const createWrapper = (props = {}) => {
wrapper = shallowMount(App, { wrapper = extendedWrapper(
propsData: { shallowMount(App, {
path: TEST_TODO_PATH, propsData: {
...props, path: TEST_TODO_PATH,
}, ...props,
}); },
})
);
}; };
// Helper methods greatly help test maintainability and readability. // Helper methods greatly help test maintainability and readability.
const findLoader = () => wrapper.find(GlLoadingIcon); const findLoader = () => wrapper.find(GlLoadingIcon);
const findAddButton = () => wrapper.find('[data-testid="add-button"]'); const findAddButton = () => wrapper.findByTestId('add-button');
const findTextInput = () => wrapper.find('[data-testid="text-input"]'); const findTextInput = () => wrapper.findByTestId('text-input');
const findTodoData = () => wrapper.findAll('[data-testid="todo-item"]').wrappers.map(wrapper => ({ text: wrapper.text() })); const findTodoData = () => wrapper.findAll('[data-testid="todo-item"]').wrappers.map(wrapper => ({ text: wrapper.text() }));
describe('when mounted and loading', () => { describe('when mounted and loading', () => {
...@@ -323,11 +336,41 @@ describe('~/todos/app.vue', () => { ...@@ -323,11 +336,41 @@ describe('~/todos/app.vue', () => {
The main return value of a Vue component is the rendered output. In order to test the component we The main return value of a Vue component is the rendered output. In order to test the component we
need to test the rendered output. Visit the [Vue testing guide](https://vuejs.org/v2/guide/testing.html#Unit-Testing). need to test the rendered output. Visit the [Vue testing guide](https://vuejs.org/v2/guide/testing.html#Unit-Testing).
### Child components
1. Test any directive that defines if/how child component is rendered (for example, `v-if` and `v-for`).
1. Test any props we are passing to child components (especially if the prop is calculated in the
component under test, with the `computed` property, for example). Remember to use `.props()` and not `.vm.someProp`.
1. Test we react correctly to any events emitted from child components:
```javascript
const checkbox = wrapper.findByTestId('checkboxTestId');
expect(checkbox.attributes('disabled')).not.toBeDefined();
findChildComponent().vm.$emit('primary');
await nextTick();
expect(checkbox.attributes('disabled')).toBeDefined();
```
1. **Do not** test the internal implementation of the child components:
```javascript
// bad
expect(findChildComponent().find('.error-alert').exists()).toBe(false);
// good
expect(findChildComponent().props('withAlertContainer')).toBe(false);
```
### Events ### Events
We should test for events emitted in response to an action within our component, this is useful to verify the correct events are being fired with the correct arguments. We should test for events emitted in response to an action within our component, this is useful to
verify the correct events are being fired with the correct arguments.
For any DOM events we should use [`trigger`](https://vue-test-utils.vuejs.org/api/wrapper/#trigger) to fire out event. For any DOM events we should use [`trigger`](https://vue-test-utils.vuejs.org/api/wrapper/#trigger)
to fire out event.
```javascript ```javascript
// Assuming SomeButton renders: <button>Some button</button> // Assuming SomeButton renders: <button>Some button</button>
...@@ -342,7 +385,8 @@ it('should fire the click event', () => { ...@@ -342,7 +385,8 @@ it('should fire the click event', () => {
}) })
``` ```
When we need to fire a Vue event, we should use [`emit`](https://vuejs.org/v2/guide/components-custom-events.html) to fire our event. When we need to fire a Vue event, we should use [`emit`](https://vuejs.org/v2/guide/components-custom-events.html)
to fire our event.
```javascript ```javascript
wrapper = shallowMount(DropdownItem); wrapper = shallowMount(DropdownItem);
...@@ -355,7 +399,8 @@ it('should fire the itemClicked event', () => { ...@@ -355,7 +399,8 @@ it('should fire the itemClicked event', () => {
}) })
``` ```
We should verify an event has been fired by asserting against the result of the [`emitted()`](https://vue-test-utils.vuejs.org/api/wrapper/#emitted) method We should verify an event has been fired by asserting against the result of the
[`emitted()`](https://vue-test-utils.vuejs.org/api/wrapper/#emitted) method.
## Vue.js Expert Role ## Vue.js Expert Role
...@@ -371,7 +416,8 @@ You should only apply to be a Vue.js expert when your own merge requests and you ...@@ -371,7 +416,8 @@ You should only apply to be a Vue.js expert when your own merge requests and you
> This section is added temporarily to support the efforts to migrate the codebase from Vue 2.x to Vue 3.x > This section is added temporarily to support the efforts to migrate the codebase from Vue 2.x to Vue 3.x
Currently, we recommend to minimize adding certain features to the codebase to prevent increasing the tech debt for the eventual migration: Currently, we recommend to minimize adding certain features to the codebase to prevent increasing
the tech debt for the eventual migration:
- filters; - filters;
- event buses; - event buses;
...@@ -382,7 +428,8 @@ You can find more details on [Migration to Vue 3](vue3_migration.md) ...@@ -382,7 +428,8 @@ You can find more details on [Migration to Vue 3](vue3_migration.md)
## Appendix - Vue component subject under test ## Appendix - Vue component subject under test
This is the template for the example component which is tested in the [Testing Vue components](#testing-vue-components) section: This is the template for the example component which is tested in the
[Testing Vue components](#testing-vue-components) section:
```html ```html
<template> <template>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment