Frontend testing standards and style guidelines

There are two types of test suites encountered while developing frontend code at GitLab. We use Jest for JavaScript unit and integration testing, and RSpec feature tests with Capybara for e2e (end-to-end) integration testing.

Unit and feature tests need to be written for all new features. Most of the time, you should use RSpec for your feature tests.

Regression tests should be written for bug fixes to prevent them from recurring in the future.

See the Testing Standards and Style Guidelines page for more information on general testing practices at GitLab.

Vue.js testing

If you are looking for a guide on Vue component testing, you can jump right away to this section.

Jest

We use Jest to write frontend unit and integration tests. Jest tests can be found in /spec/frontend and /ee/spec/frontend in EE.

Limitations of jsdom

Jest uses jsdom instead of a browser for running tests. This comes with a number of limitations, namely:

See also the issue for support running Jest tests in browsers.

Debugging Jest tests

Running yarn jest-debug runs Jest in debug mode, allowing you to debug/inspect as described in the Jest docs.

Timeout error

The default timeout for Jest is set in /spec/frontend/test_setup.js.

If your test exceeds that time, it fails.

If you cannot improve the performance of the tests, you can increase the timeout for a specific test using setTestTimeout.

import { setTestTimeout } from 'helpers/timeout';

describe('Component', () => {
  it('does something amazing', () => {
    setTestTimeout(500);
    // ...
  });
});

Remember that the performance of each test depends on the environment.

Test-specific stylesheets

To help facilitate RSpec integration tests we have two test-specific stylesheets. These can be used to do things like disable animations to improve test speed, or to make elements visible when they need to be targeted by Capybara click events:

  • app/assets/stylesheets/disable_animations.scss
  • app/assets/stylesheets/test_environment.scss

Because the test environment should match the production environment as much as possible, use these minimally and only add to them when necessary.

What and how to test

Before jumping into more gritty details about Jest-specific workflows like mocks and spies, we should briefly cover what to test with Jest.

Don’t test the library

Libraries are an integral part of any JavaScript developer’s life. The general advice would be to not test library internals, but expect that the library knows what it’s supposed to do and has test coverage on its own. A general example could be something like this

import { convertToFahrenheit } from 'temperatureLibrary'

function getFahrenheit(celsius) {
  return convertToFahrenheit(celsius)
}

It does not make sense to test our getFahrenheit function because underneath it does nothing else but invoking the library function, and we can expect that one is working as intended. (Simplified, I know)

Let’s take a short look into Vue land. Vue is a critical part of the GitLab JavaScript codebase. When writing specs for Vue components, a common gotcha is to actually end up testing Vue provided functionality, because it appears to be the easiest thing to test. Here’s an example taken from our codebase.

// Component script
{
  computed: {
    hasMetricTypes() {
      return this.metricTypes.length;
    },
}
<!-- Component template -->
<template>
  <gl-dropdown v-if="hasMetricTypes">
    <!-- Dropdown content -->
  </gl-dropdown>
</template>

Testing the hasMetricTypes computed prop would seem like a given here. But to test if the computed property is returning the length of metricTypes, is testing the Vue library itself. There is no value in this, besides it adding to the test suite. It’s better to test a component in the way the user interacts with it: checking the rendered template.

// Bad
describe('computed', () => {
  describe('hasMetricTypes', () => {
    it('returns true if metricTypes exist', () => {
      factory({ metricTypes });
      expect(wrapper.vm.hasMetricTypes).toBe(2);
    });

    it('returns true if no metricTypes exist', () => {
      factory();
      expect(wrapper.vm.hasMetricTypes).toBe(0);
    });
  });
});

// Good
it('displays a dropdown if metricTypes exist', () => {
  factory({ metricTypes });
  expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
});

it('does not display a dropdown if no metricTypes exist', () => {
  factory();
  expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
});

Keep an eye out for these kinds of tests, as they just make updating logic more fragile and tedious than it needs to be. This is also true for other libraries. A suggestion here is: if you are checking a wrapper.vm property, you should probably stop and rethink the test to check the rendered template instead.

Some more examples can be found in the Frontend unit tests section

Don’t test your mock

Another common gotcha is that the specs end up verifying the mock is working. If you are using mocks, the mock should support the test, but not be the target of the test.

const spy = jest.spyOn(idGenerator, 'create')
spy.mockImplementation = () = '1234'

// Bad
expect(idGenerator.create()).toBe('1234')

// Good: actually focusing on the logic of your component and just leverage the controllable mocks output
expect(wrapper.find('div').html()).toBe('<div id="1234">...</div>')

Follow the user

The line between unit and integration tests can be quite blurry in a component heavy world. The most important guideline to give is the following:

  • Write clean unit tests if there is actual value in testing a complex piece of logic in isolation to prevent it from breaking in the future
  • Otherwise, try to write your specs as close to the user’s flow as possible

For example, it’s better to use the generated markup to trigger a button click and validate the markup changed accordingly than to call a method manually and verify data structures or computed properties. There’s always the chance of accidentally breaking the user flow, while the tests pass and provide a false sense of security.

Common practices

These some general common practices included as part of our test suite. Should you stumble over something not following this guide, ideally fix it right away. 🎉

How to query DOM elements

When it comes to querying DOM elements in your tests, it is best to uniquely and semantically target the element.

Preferentially, this is done by targeting what the user actually sees using DOM Testing Library. When selecting by text it is best to use the byRole query as it helps enforce accessibility best practices. findByRole and the other DOM Testing Library queries are available when using shallowMountExtended or mountExtended.

When writing Vue component unit tests, it can be wise to query children by component, so that the unit test can focus on comprehensive value coverage rather than dealing with the complexity of a child component’s behavior.

Sometimes, neither of the above are feasible. In these cases, adding test attributes to simplify the selectors might be the best option. A list of possible selectors include: