Vue.js/Testing

Vue.js encourages you to break up the elements of a UI into a common set of isolated, reusable components that are responsible for their own content and behavior. Additionally, these components rely on a virtual DOM and typically have little need for direct reliance on browser-specific APIs. UIs built with Vue.js are thus well-suited for unit-testing in a headless, Node.js-based environment. In many cases it is possible (and easy!) to test behaviors from the command-line that could previously only be captured in end-to-end, Selenium-style tests (clicks, key events, and other simulated user interactions).

This page is intended as a high-level guide for how to test Vue.js code in a MediaWiki environment. Many of the tools relied on here (Jest, vue-test-utils, etc.) have extensive documentation of their own which is worth studying as well.

Getting started
This guide will cover how to use Vue's official testing library with Jest in the context of a MediaWiki extension. Testing Vue code in Core should be similar but some adjustments may be needed.

Jest
Jest is a "batteries-included" test runner developed by Facebook. In addition to the  test runner itself, the software includes an expectation library and extensive mocking capabilities (there is no need to use Sinon alongside Jest, for example). Jest also sets up a JSDOM environment automatically.

One of the biggest advantages of using Jest with Vue is that it is possible to test single-file Vue components ( files) without relying on a bundling tool like Webpack, by using the   pre-processor. Since some teams will be shipping ES5 Vue components that don't rely on Webpack, this should help to reduce unnecessary complexity in test setup.

Vue Test Utils
Vue Test Utils is the official unit testing utility library for Vue.js. The project website contains extensive documentation (both API-level and a more narrative "guide").

Setup
You will need to add the following libraries to  as  :


 * (required if using without a build-step)
 * (required if using without a build-step)
 * (required if using without a build-step)
 * (required if using without a build-step)
 * (required if using without a build-step)
 * (required if using without a build-step)

A standard test script in  might look like this:

In addition to these libraries and some sort of testing script, you will need to add two top-level files:  (configuration properties) and   (code to be run before starting the test suite, to set up an appropriate environment).

Jest config file
This file defines Jest's configuration. Jest ships with reasonable defaults here, but there are couple of properties which are worth knowing about:

Vue-Jest configuration
The following properties should be added to get Jest to transform Vue single-file components:

Coverage
To generate Istanbul-style coverage reports, the following config properties are needed:

Specify setup file
See the next section for an example of how to use a setup file.

Jest setup file
Jest allows you to define a setup file which is run before the start of the entire test suite. This is useful for setting up a MediaWiki browser environment, where a lot of objects tend to be exposed in the global namespace. Your setup file can load real code or Jest mock functions as needed. The example below sets up jQuery, OOJS, and OOUI using NPM packages and then stubs out a global MW object with methods that may need to exist during testing:

Simulating a MediaWiki environment
By using a Jest setup file (see above) it is possible to fake most of the properties/methods added to the global scope by MW Core, other extensions, etc. A lot of the mocking code from the mw-node-qunit library could be easily adapted here, for example.

Mocking resource modules
In addition to mocking out aspects of the MediaWiki browser environment, you may need to mock entire modules that are loaded via  in ResourceLoader. Some Wikibase dependencies may need to be mocked in this way, for example. This is also useful if ResourceLoader exposes code with a module name that differs from the name of the equivalent NPM package ( vs   for example).

Example

Let's say that our code depends on a module that is provided by ResourceLoader at runtime: var datamodel = require( 'wikibase.datamodel' ); In the test environment, we don't have access to ResourceLoader, so this will be interpreted as a node module. But our  folder (which is where the call to   will look when we run the tests) doesn't contain a module by this name.

Jest makes it easy to mock this module dependency by creating a  folder in the same directory as where the module would normally be found (in this case, that would be in the root directory of the extension, adjacent to  ). Inside the __mocks__ folder, you can add a JS file with a name that matches the name of the module:. Jest will automatically load this file instead of attempting to find the real module in node_modules.

The contents of the mock file could be anything. If we didn't care about what any of this code did in our tests and just needed to stub out a few dummy functions, we could so something like this: More information about mocking modules in Jest can be found here: https://jestjs.io/docs/en/manual-mocks

Example Tests
Using the Wrapper object

Say we have a simple App.vue component: Since Vue components rely on a virtual DOM, it is easy to test for the presence or absence of HTML elements, attributes, classes, etc. without relying on a browser environment: Most tests will involve a wrapper object, which is created by calling  or   with the component being tested as an argument. This test uses the wrapper's built-in contains method to find an element that matches a given selector.

Testing async updates with Vue.nextTick

Here's a slightly more complex test for the same component that ensures the contents of the  tag are always in sync with the value of the component's   data (local state). Vue batches DOM updates asynchronously for efficiency. To ensure that something happens after these updates are complete, use. Our assertion runs in this callback to ensure that Vue's DOM upates have completed before we look for the updated text.

Mount vs ShallowMount
Vue Test Utils provides two ways to mount components during testing:  and. Most of the time,  is preferable, because it allows you to mount a component in isolation from any child components it may contain (they are replaced with stubs). This approach is faster and lets us write tests that focus on a single component at a time.

In cases where you do want child components to be created alongside the parent, use the  method instead.

More information on shallow rendering can be found here: https://vue-test-utils.vuejs.org/guides/#shallow-rendering

Simulating user interaction
The  object exposes a trigger method that can be used to programmatically trigger DOM events. Here's an example of asserting that a particular method is called when a button is clicked:

Testing plugins
If the component you are testing relies on features injected by a global plugin (like the  plugin included in core), you can create a local modified copy of the Vue constructor by using the createLocalVue method from Vue Test Utils. This is also useful for testing components that rely on Vuex or Vue-router.

Testing Vuex stores
The contents of a Vuex store are really just plain JavaScript functions and objects. This makes it relatively straightforward to test Vuex code in a Vue application. Tests for a Vuex store should primarily focus on mutations and actions, since that's where most of the logic will likely reside in any given store.

Testing Mutations and Getters
Mutations lend themselves to unit testing because they must be synchronous and they should not have any side-effects. They always take a state object as their first argument and an optional payload as a second argument.

Here's an example of a basic mutation test in Jest: Tests for Getters will work more or less the same way since getter functions simply return a new value based on existing values in the state.

Testing Actions
Actions will often house some of the more complex business logic in an application: API requests, interactions with other services, etc. Actions can be asynchronous as well. However, at the end of the day any given action should be responsible for committing one or more mutations (the only way data in the store can actually be changed). One way to keep action tests from becoming too complex is to focus on these commits, and to use mocks to represent external systems that lie outside the scope of the current test.

Here's an example of what a test for an asynchronous action that fetches data from some API. To test this action in isolation, we can use Jest mocks to simulate the API under various conditions (successful request, failed request, etc). The goal of the test is to ensure that the correct mutations are called in the correct circumstances.

More information about testing mutations, getters, and actions can be found here: https://vue-test-utils.vuejs.org/guides/using-with-vuex.html#testing-getters-mutations-and-actions-separately