Jest unit tests in Shopware's administration

Overview

You should write a unit test for every functional change. It should guarantee that your written code works and that a third developer can't break the functionality with their code.

With a good test coverage we can have the confidence to deploy a stable software without needing to manually test the software in its entirety. This little guide will guide you how to write unit tests for the administration in Shopware 6.

We are using Jest as our testing framework. It's a solid foundation and widely used by many developers. Before you are reading this guide you have to make sure you understand the basics of unit tests and how Jest works.

Video

Did you know that there's a video available to this topic? Please take a look:

Prerequisites

This tutorial will have a strong focus on how unit tests should be written when it comes to components in the administration. So please make sure you already know what a unit test is and why we are doing it. Furthermore, you should know what components tests are and what we want to achieve with them. You can find a good source for best practices in this Github repository:

In addition, you need a running Shopware 6 installation. Your repository used for that should be based on development template, as we need to use some scripts provided by it.

Folder structure

The test folder structure should match the source folder structure. You add a test for a file in the same path as the source path. You see: When creating the file, the name should also be the same as the component has with an additional .spec before the file extension .js. The .spec suffix is a well-known naming convention of frontend testing files.

Resources
`-- app
`-- <environment>
`-- test
`-- app
`-- component
`-- base
`-- sw-alert.spec.js

Please note that in this example, <environment> is a placeholder for the environment you are working in. In your context, that should be administration.

Testing services and ES modules

Services and isolated ECMAScript modules are well testable because you can import them directly without mocking or stubbing dependencies. A service can be used isolated and therefore is easy to test.

Let's have a look at an example:

sanitizer.helper.spec.js
import Sanitizer from 'src/core/helper/sanitizer.helper';
​
describe('core/helper/sanitizer.helper.js', () => {
it('should sanitize the html', () => {
expect(Sanitizer.sanitize('<A/hREf="j%0aavas%09cript%0a:%09con%0afirm%0d``">z'))
.toBe('<a href="j%0aavas%09cript%0a:%09con%0afirm%0d``">z</a>');
});
​
it('should remove script functions from dom elements', () => {
expect(Sanitizer.sanitize('<details open ontoggle=confirm()>'))
.toBe('<details open=""></details>');
});
​
it('should remove script functions completely', () => {
expect(Sanitizer.sanitize(`<script y="><">/*<script* */prompt()</script`))
.toBe('');
});
​
it('should sanitize js in links', () => {
expect(Sanitizer.sanitize('<a href=javas&#99;ript:alert(1)>click'))
.toBe('<a>click</a>');
});
​
// ...more tests
});

You see, you are able to write the test the same way you're used to, writing Jest unit tests in general.

Write tests for components

After setting up your component test, you need to write your tests. A good way to write them is to test input and output. The most common tests are:

  • set Vue Props and check if component looks correctly

  • interact with the DOM and check if the desired behaviour is happening

However, when it comes to writing component tests for Shopware's administration, there are some further steps to go. We will take a look at them in the following paragraphs.

Setup for testing Vue components

We are using the Vue Test Utils for easier testing of Vue components. If you don't have experience with testing Vue components it is useful to read some basic guides on this topic. The main part of testing components is similar in Shopware 6.

However, there are some important differences. We can't test components that easily like in other Vue projects because we are supporting template inheritance and extendability for third party developers. This causes overhead which we need to bear in mind.

We are using a global object as an interface for the whole administration. Every component gets registered to this object, e.g. Shopware.Component.register(). Therefore, we have access to Component with the Shopware.Component.build() method. This creates a native Vue component with a working template. Every override and extension from another components are resolved in the built component.

Executing tests

Before you are using the commands make sure that you installed all dependencies for your administration. If you haven't done this already, then you can do it running the following PSH command: ./psh.phar administration:install-dependencies

In order to run jest unit tests of the administration, you can use the psh commands provided by our development template.

This only applies to the Shopware provided Administration! If you use unit tests in your plugin, you might need to write your own scripts for that.

This command executes all unit tests and shows you the complete code coverage. ./psh.phar administration:unit

This command executes only unit tests of changed files. It automatically restarts if a file gets saved. This should be used during the development of unit tests. ./psh.phar administration:unit-watch

First example: Testing sw-multi-select component

For better understanding how to write component tests for Shopware 6 let's write a test. In our example we are using the component sw-multi-select.

When you want to mount your component it needs to be imported first:

test/app/component/form/select/base/sw-multi-select.spec.js
import 'src/app/component/form/select/base/sw-multi-select';

You see that we import the sw-multi-select without saving the return value. This blackbox import only executes code. However, this is important because this registers the component to the Shopware object:

src/app/component/form/select/base/sw-multi-select/index.js
Shopware.Component.register('sw-multi-select', {
// The vue component
});

Mounting components

In the next step we can mount our Vue component which we get from the global Shopware object:

test/app/component/form/select/base/sw-multi-select.spec.js
import 'src/app/component/form/select/base/sw-multi-select';
​
shallowMount(Shopware.Component.build('sw-multi-select'));

When we’re testing our vue.js components, we need a way to mount and render the component. Therefore, we use the following methods:

  • mount(): Creates a Wrapper that contains the mounted and rendered Vue component.

  • shallowMount(): Like mount, it creates a Wrapper that contains the mounted and rendered Vue component,

    but with stubbed child components.

This way, we create a new wrapper before each test. The build method resolves the twig template and returns a vue component.

Test structure

Now you can test the component like any other component. Let's try to write our first test:

test/app/component/form/select/base/sw-multi-select.spec.js
import { shallowMount } from '@vue/test-utils';
import 'src/app/component/form/select/base/sw-multi-select';
​
describe('components/sw-multi-select', () => {
let wrapper;
​
beforeEach(() => {
wrapper = shallowMount(Shopware.Component.build('sw-multi-select'));
});
​
afterEach(() => {
wrapper.destroy();
});
​
it('should be a Vue.js component', () => {
expect(wrapper.vm).toBeTruthy();
});
});

This contains our component. In our first test we only check if the wrapper is a Vue instance.

Running the test

Now lets start the watcher to see if the test works. You can do this with our PSH command ./psh.phar administration:unit-watch. You should see a result like this: Test Suites: 1 passed, 1 total. You should also see several warnings like this:

  • [Vue warn]: Missing required prop: "options"

  • [Vue warn]: Missing required prop: "value"

  • [Vue warn]: Unknown custom element: <sw-select-base> - did you register the component correctly? ...

The first two warnings are solved easily by providing the required props to our shallowMount:

wrapper = shallowMount(Shopware.Component.build('sw-multi-select'), {
props: {
options: [],
value: ''
}
});

Now you should only see the last warning with an unknown custom element. The reason for this is that most components contain other components. In our case the sw-multi-select needs the sw-select-base component. Now we have several solutions to solve this. The two most common ways are stubbing or using the component.

test/app/component/form/select/base/sw-multi-select.spec.js
import 'src/app/component/form/select/base/sw-select-base';
​
wrapper = shallowMount(Shopware.Component.build('sw-multi-select'), {
props: {
options: [],
value: ''
},
stubs: {
'sw-select-base': Shopware.Component.build('sw-select-base'),
}
});

You need to choose which way is needed: Many tests do not need the real component, but in our case we need the real implementation. You will see that if we import another component that they can create also warnings. Let's look at the code that solve all warnings, then we should have a code like this:

test/app/component/form/select/base/sw-multi-select.spec.js
import { shallowMount } from '@vue/test-utils';
import 'src/app/component/form/select/base/sw-multi-select';
import 'src/app/component/form/select/base/sw-select-base';
import 'src/app/component/form/field-base/sw-block-field';
import 'src/app/component/form/field-base/sw-base-field';
import 'src/app/component/form/field-base/sw-field-error';
import 'src/app/component/form/select/base/sw-select-selection-list';
import 'src/app/component/form/select/base/sw-select-result-list';
import 'src/app/component/utils/sw-popover';
import 'src/app/component/form/select/base/sw-select-result';
import 'src/app/component/base/sw-highlight-text';
import 'src/app/component/base/sw-label';
import 'src/app/component/base/sw-button';
​
describe('components/sw-multi-select', () => {
let wrapper;
​
beforeEach(() => {
wrapper = shallowMount(Shopware.Component.build('sw-multi-select'), {
props: {
options: [],
value: ''
},
stubs: {
'sw-select-base': Shopware.Component.build('sw-select-base'),
'sw-block-field': Shopware.Component.build('sw-block-field'),
'sw-base-field': Shopware.Component.build('sw-base-field'),
'sw-icon': '<div></div>',
'sw-field-error': Shopware.Component.build('sw-field-error'),
'sw-select-selection-list': Shopware.Component.build('sw-select-selection-list'),
'sw-select-result-list': Shopware.Component.build('sw-select-result-list'),
'sw-popover': Shopware.Component.build('sw-popover'),
'sw-select-result': Shopware.Component.build('sw-select-result'),
'sw-highlight-text': Shopware.Component.build('sw-highlight-text'),
'sw-label': Shopware.Component.build('sw-label'),
'sw-button': Shopware.Component.build('sw-button')
}
});
});
​
afterEach(() => {
wrapper.destroy();
});
​
it('should be a Vue.js component', () => {
expect(wrapper.vm).toBeTruthy();
});
});

Second example: Testing of message inside the sw-alert component

Of course, the complexity and structure of your test depends on what you are trying to achieve with your component. Here is one little example concerning the component sw-alert: Actually, he task of an alert is displaying a message for the user in most cases. So in this example, let's write a test for this text located in a slot. You can find this example in the linked video above as well.

We will start with an already written test similar to the first example:

import { shallowMount } from '@vue/test-utils';
import 'src/app/component/base/sw-alert';
​
describe('components/base/sw-alert', () => {
it('should be a Vue.js component', () => {
const wrapper = shallowMount(Shopware.Component.build('sw-alert'), {
stubs: ['sw-icon']
});
​
// Assert if our component is a vue instance = mountes correctly
expect(wrapper.vm).toBeTruthy();
});
});

There we'll add another test case for testing the alert's message:

import { shallowMount } from '@vue/test-utils';
import 'src/app/component/base/sw-alert';
​
describe('components/base/sw-alert', () => {
it('should be a Vue.js component', () => {
// see above
});
​
it('should render the message inside the default slot', () => {
// New
});
});

Mounting components

You can set the content of a slot during component mount. See the paragraph "Mounting components" in the first example for details.

it('should render the message inside the default slot', () => {
const wrapper = shallowMount(Shopware.Component.build('sw-alert'), {
slots: {
default: 'My custom message'
}
});
});

Afterwards you can make an assertion that the text passed to the slot will be rendered inside the desired element. In this case we search in the wrapper for the element with the selector .sw-alert__message and check if the text is there:

it('should render the message inside the default slot', () => {
const wrapper = shallowMount(Shopware.Component.build('sw-alert'), {
slots: {
default: 'My custom message'
}
});
expect(wrapper.find('.sw-alert__message').text()).toBe('My custom message');
});

Stubbing your component

Vue Test Utils has some advanced features for stubbing components. A stub is actually when you replace an existing implementation of a custom component with a dummy component doing nothing at all, actually. This is necessary for the component to function independently, in an isolated way.

Components in Shopware might also depend on other dependencies like $tc, directives or injections. This way, the setup of your test may get more complex. When you are building components then you need to mock their dependencies as well.

To improve the test writing experience we included many mocks, helper methods and even more by default. This will help you to reduce the overhead of setting up a single test with all mocks.

Everything can be overwritten in the mount or shallowMount method if you need to have custom implementation.

Using preconfigured mocks

ACL

You can set the active ACL roles by simply adding values to the global variable global.activeAclRoles. By default, the test suite has no ACL rights. If you want, you can change the privileges for each test separately.

Example:

it('should render with ACL rights', async () => {
// set ACL privileges
global.activeAclRoles = ['product.editor'];
​
const wrapper = await createWrapper();
expect(wrapper.vm).toBeTruthy();
});

Feature flags

If you want to enable feature flags you can add the flag to the global variable global.activeFeatureFlags. If you want to, you can change the usage of feature flags for each test.

Example:

it('should render with active feature flag', async () => {
// set feature flag
global.activeFeatureFlags = ['FEATURE_NEXT_12345'];
​
const wrapper = await createWrapper();
expect(wrapper.vm).toBeTruthy();
});

Repository factory

The data handling and the repository factory works by default. It will be generated by the entity-schema which will be written to a file before you start the test suite.

Every time the repository factory requests something from a URL, you get a notification in the console. This notification also includes a short guide on how to implement the response. This information may look like this:

// You should implement mock data for this route: "/search/product".
​
/*
* ############### Example ###############
*/
​
const responses = global.repositoryFactoryMock.responses;
​
responses.addResponse({
method: 'Post',
url: '/search/product',
status: 200,
response: {
data: [
{
id: YourId,
attributes: {
id: YourId
}
}
]
}
});
​
/*
* ############### Example ###############
*/
​
// You can disable this warning with this code:
​
global.repositoryFactoryMock.showWarning = false;

The response value should contain your test data. It needs to match the response from the backend API. An easy way to get the correct response is to inspect the response from the real API when you open the administration.

If you don't want to use this helper then you can easily overwrite it by setting a custom mock for the repositoryFactory in your mount method.

Directives

All global directives are registered by default. You can overwrite them if you want.

Filters

All global filters are registered by default. You can overwrite them if you want.

Services

Some services are registered with a mock alternative. If you want to use a different service, you need to mock it manually. The console will inform you with a warning that the service does not exist.

Context

The global Shopware context is prepared automatically. You can overwrite them in Shopware.Store if necessary.

Global mocks

For most cases we created automatic mocks, so you donΒ΄t need to implement them manually. Some examples are $tc, $device, $store or $router.

If you want to override one mock then you can do it in the mount method:

mount('dummy-component', {
mocks: {
$tc: (...args) => JSON.stringify([...args])
}
})

Using mocks

A common warning can occur if you didn't mock functions needed in your component. For example, please look at the warning below:

[Vue warn]: Error in render: "TypeError: hasError is not a function"

The solution is using mocks while mounting your component. In this case, your mock here is a simple function returning the value "true".

shallowMount(Shopware.Component.build('your-component'), {
mocks: {
hasError: () => false // your mock (here a simple function returning the value "true")
}
});

Stubbing directives

When working with components of Shopware's Administration, you might stumble upon the following warning:

[Vue warn]: Failed to resolve directive: clipboard

If that happens, you need to use localVue to provide the directive mock. createLocalVue returns a Vue class for you to add components, mixins and install plugins without polluting the global Vue class. In our context, it looks like this:

import { shallowMount, createLocalVue } from '@vue/test-utils';
​
const localVue = createLocalVue();
localVue.directive('clipboard', {}); // add directive mock to localVue
​
shallowMount(Shopware.Component.build('your-component'), {
localVue
});

Stubbing injections

Another common warning is the one below:

[Vue warn]: Injection "mediaService" not found

The solution is stubbing this injection. In detail, you need to provide the injected data, services and so on: That means you need to mock the return values for your test's methods used, e.g. renameMedia and uploadMediaById - Providing the data exactly as needed by the component for running your test case and what the injection actually is. It can be valid to set the injection name equal to an empty object just to mute it when it's not really needed for your test.

provide: {
mediaService: {}
}

Please note that the mediaService has a renameMedia method. So in order to stub the mediaService in realistic manner, follow the example below:

mediaService: {
renameMedia: () => Promise.resolve()
}

In the context of injections, the next warning can occur sometimes:

[Vue warn]: Error in foo: "TypeError: Cannot read property 'renameMedia' of undefined"

This can be caused by several reasons. The best way to find out the solution is to look at the source of the code and find out what is missing. This is highly dependent on the component under test, though. In this example the service mediaService is missing:

Shopware.Service('mediaService').renameMedia(mediaId, newFileName);

To fix this you need to add the mocked service before all tests. In our case we need to register the service:

beforeAll(() => {
Shopware.Service.register('mediaService', {
// your service mock
});
});

Next steps

Do you want to see these examples in practice? Head over to our video tutorial on how to write component tests in jest for the Shopware administration.

Furthermore, you might want to have a look at one of the following guides as well: