Skip to content

Instantly share code, notes, and snippets.

@belsrc
Last active August 4, 2023 16:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save belsrc/3342dfda347dc89dcdb9a9c47f262e19 to your computer and use it in GitHub Desktop.
Save belsrc/3342dfda347dc89dcdb9a9c47f262e19 to your computer and use it in GitHub Desktop.
Javascript Testing Best Practices Readme in Relation to Components

Setup

I'm not going to go too far into the setup as it is not much different from any other normal Jest setup. And they have plenty of documentation covering it already. In the component test files themselves, you will need to make sure you import vue-test-utils and its probably a good idea to set some clean up in the beforeEach handler.

import { shallowMount } from '@vue/test-utils';
import Component from './index.jsx';

describe('Component', () => {
  beforeEach(() => {
    jest.resetModules();
    jest.clearAllMocks();
  });
  // ....
});

You will largely just be using the shallowMount and occasionally the mount exports from vue-test-utils. The shallowMount, as the name implies, will only render the current top-level component that is under test. All children will be simple stubs, as an example, El UI's Button component would be something like <el-button></el-button>. mount on the other hand, will render the top level and everything below it.

Component Testing

With the use of vue-test-utils, testing most components is relatively straightforward. Mount it, set values and test results. Using the test utils you have direct access to the entire component by using the wrappers vm property.

Testing static and computed properties is similar to checking a property on any other object.

test('dailyDisabled should be true when at the daily limit and not checked', () => {
  const wrapper = shallowMount(WordItem, { propsData: { word, tracked: 1000 }});

  const actual = wrapper.vm.disabled;

  expect(actual).toBeTruthy();
});

You can test all aspects of static props as well, using the vm's $options.

test('expect value property config to be correct', () => {
  const wrapper = shallowMount(PageSelect);
  const valueProp = wrapper.vm.$options.props.value;

  expect(valueProp.required).toBeTruthy();
  expect(valueProp.type).toEqual([Boolean, Number, String]);
  expect(valueProp.default).toEqual(null);
});

Though I find this to be excessive and unneeded as you are actually testing whether the framework is working, which has been thoroughly tested already. The one aspect that could be tested, and holds value, is complex validators that are on the property.

const actual = valueProp.validator('a');

expect(actual).toBeFalsy()

Testing methods, like props, is just like testing normal object methods.

test('addHourly should add item to the end of hourly list', () => {
  const words = ['test case'];
  const wrapper = shallowMount(TrackList, { propsData: { words } });

  wrapper.vm.addWord({ phrase: 'unit test' });

  expect(wrapper.vm.words).toEqual([ ...words, 'unit test' ]);
});

When you start to get into testing the component rendering this is where it slightly deviates. But, thanks again to the test utils, it is simple in its execution. You simply need to mount the component and then the wrapper becomes, for a lack of a better comparison, a jQuery object.

test('should not show disabled message when not disabled', () => {
  const wrapper = mount(WordTracker, { propsData: { disabled: false }});

  const actual = wrapper.find('.word-track__disable-msg').exists();

  expect(actual).not.toBeTruthy();
});

The wrapper automatically keeps track of all events that the component emits. This allows you to check if an event was emitted from the component by searching for it in the wrappers emitted prop. (There's a couple of different ways to do this but the one below I've found to be the most readable and straighforward.)

test('should emit click event when button clicked', () => {
  const wrapper = mount(WordTracker, { propsData: { disabled: false }});

  wrapper.find('.word-track__edit').trigger('click');

  expect(wrapper.emitted('click')).toBeTruthy();
});

You can also check the events payload.

test('should emit correct value when clicked', () => {
  const wrapper = mount(WordTracker, { propsData: { disabled: false }});
  const expected = {};

  wrapper.find('.word-track__save').trigger('click');

  expect(wrapper.emitted('click')[0]).toEqual([expected]);
});

EDIT: This will be chaning in a future release - vuejs/vue-test-utils#1137

Functional Components

Testing functional components can be a bit odd. As they have no context a lot of the methods on vue-test-utils will not work. To reliably test them, they need to be wrapped in an additional component. But then you also need to be able to pass down the props and event handlers through the wrapper component and into the actual component under test. This was becoming bothersome after two components so I made a lib that does that wrapping and passing for you.

import { mount } from '@vue/test-utils';
import wrapFunctional from '@belsrc/vue-test-functional-wrapper';
import WordTracker from './index.jsx';

let wrapped;

describe('WordTracker', () => {
  beforeEach(() => {
    jest.resetModules();
    jest.clearAllMocks();
    wrapped = wrapFunctional(WordTracker, {
      methods: { click() { this.$emit('click'); } },
      on(vm) { return { click: vm.click } },
    });
  });
  // ...
});

State-aware Components

When testing components that access state there is quit a bit more setup. You need to add additional imports (any store types, Vuex and createLocalVue) as well as manually set up the state. You dont need to set up the full state that the application has, just what the component uses. Unlike normal components, when you test with state you need to set up a local Vue instance to use Vuex.

import { createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';

const localVue = createLocalVue();
localVue.use(Vuex);

describe('ProductList', () => {
  // ...
});

And then since you might mutate the state during the cource of testing you should setup the state so that it is fresh for each test. The easiest way, that I've found, is inside the base describe add some mutatable variables to hold the current state.

describe('ProductList', () => {
  let state;
  let actions;
  let mutations;
  let store;

  // ...

And then set those values in the in the beforeEach handler.

beforeEach(() => {
  jest.resetModules();
  jest.clearAllMocks();
  state = {
    isLoading: false,
    products: [...products],
    variations: [...variations],
  };
  actions = {
    [LOAD_PRODUCTS]: jest.fn(),
    [LOAD_PRODUCT_VARIATIONS]: jest.fn(),
  };
  mutations = {
    [SET_PRODUCTS]: jest.fn(),
  };
  store = new Vuex.Store({
    state,
    actions,
    mutations,
  });
});

As above, the additional work flows into the individual unit tests.

If you use stubs like above, you will be checking against the mock function using toHaveBeenCalled().

test('loadProducts should call LOAD_PRODUCTS action', () => {
  const wrapper = shallowMount(ProductList, { store, localVue });
  wrapper.vm.loadProducts(false);

  const actual = actions[LOAD_RULE_PRODUCTS];

  expect(actual).toHaveBeenCalled();
});

If you don't use mock functions, and instead let it flow to state, you can just check the store bound values like you do for any other prop. Though doing this will more than likely cause you to have to mock additional imports that are found in the actions.

When working with module state, it is almost identical to the above except you just nest the state down like it would be in the application.

beforeEach(() => {
  jest.resetModules();
  jest.clearAllMocks();
  state = {
    isLoading: false,
    products: [...products],
    variations: [...variations],
  };
  actions = {
    [LOAD_PRODUCTS]: jest.fn(),
    [LOAD_PRODUCT_VARIATIONS]: jest.fn(),
  };
  mutations = {
    [SET_PRODUCTS]: jest.fn(),
  };
  store = new Vuex.Store({
    state: {},
    modules: {
      products: {
        state,
        actions,
        mutations,
      },
    },
  });
});

Good Practices

The Golden Rule: Design for lean testing

Testing code is not like production-code - design it to be dead-simple, short, abstraction-free, delightful to work with, lean. One should look at a test and get the intent instantly.

To that end, I usually set up my test files to clearly describe the component that is under test as well as the area of the component that is under test. And in those sections, your actual tests. Usually, they are in pretty much the same order as they are in the component. This way they maintain a nice simple structure without the need to dig through computed prop tests next to render tests next method tests throughout the file. And scanning the file becomes easier when you need to find a failing test or add an additional one. if you are using vue-gen (shameless plug), all of this handled automatically.

Example
describe('ComponentUnderTest', () => {
  beforeEach(() => { /*...*/ });

  describe('Properties', () => { /*...*/ });

  describe('Computed', () => { /*...*/ });

  describe('Methods', () => { /*...*/ });

  describe('Rendering', () => { /*...*/ });
});

Include 3 parts in each test name

A test report should tell whether the current application revision satisfies the requirements for the people who are not necessarily familiar with the code: the tester, the DevOps engineer who is deploying and the future you two years from now. This can be achieved best if the tests speak at the requirements level and include 3 parts:

  1. What is being tested? For example, the Product component
  2. Under what circumstances and scenario? For example, no image source is passed to the component
  3. What is the expected result? For example, the component renders a placeholder

With the above test file layout structure, #1 can be omitted as when the tests are ran the result will appear in a simple tree structure so it holds slightly less value. Though it should be noted that in a testing framework that doesn't allow description blocks, it is still important. The order is less important as I usually try to make the name a coherent sentence so #2 and #3 are usually swapped.

Example
describe('Rendering', () => {
  test('should render placeholder image when no src provided', () => {
    const wrapper = shallowMount(Product);

    const actual = wrapper.find('.product__placeholder-image').exists();

    expect(actual).toBeTruthy();
  });
});

Structure tests by the AAA pattern

Structure your tests with 3 well-separated sections Arrange, Act & Assert (AAA). Following this structure guarantees that the reader spends no mental effort on understanding the test plan:

  1. Arrange: All the setup code to bring the system to the scenario the test aims to simulate. This is usually mounting the component and setting any component state data.
  2. Act: Execute the unit under test. Usually 1 line of code
  3. Assert: Ensure that the received value satisfies the expectation. Usually 1 line of code
Example
test('should not show disabled message when disabled is false', () => {
  // Arrange
  const wrapper = mount(Component, { propsData: { disabled: false }});

  // Act
  const actual = wrapper.find('.count__disable-msg').exists();

  // Assert
  expect(actual).not.toBeTruthy();
});

Describe expectations in a product language

Coding your tests in a declarative-style allows the reader to get the grab instantly without any mental load. When you write an imperative code that is packed with conditional logic the reader is thrown away to an effortful mental mood.

Stick to black-box testing: Test only public methods

Testing the internals brings huge overhead for almost nothing. If your code/API deliver the right results, should you invest your next 3 hours in testing HOW it worked internally and then maintain these fragile tests? Whenever a public behavior is checked, the private implementation is also implicitly tested and your tests will break only if there is a certain problem. This approach is also referred to as behavioral testing. On the other side, should you test the internals (white-box approach) — your focus shifts from planning the component outcome to nitty-gritty details and your test might break because of minor code refactors although the results are fine— this dramatically increases the maintenance burden. For components, this revolves around testing the rendering behavior. If the component renders a button if a computed/local state prop is true and a message otherwise, you can test that each is rendered under each condition instead of testing HOW each is shown.

Choose the right test doubles: Avoid mocks in favor of stubs and spies

Test doubles are a necessary evil because they are coupled to the application internals, yet some provide immense value. However, the various techniques were not born equal: some of them, spies and stubs, are focused on testing the requirements but as an inevitable side-effect they also slightly touch the internals. Mocks, on the contrary, are focused on testing the internals — this brings huge overhead as explained in “Stick to black-box testing”.

Don’t “foo”, use realistic input data

Often production bugs are revealed under some very specific and surprising input — the more realistic the test input is, the greater the chances are to catch bugs early. Use dedicated libraries like Faker to generate pseudo-real data that resembles the variety and form of production data. For example, such libraries can generate realistic phone numbers, usernames, credit card, company names, and even ‘lorem ipsum’ text. You may also create some tests (on top of unit tests, not instead) that randomize fakers data to stretch your unit under test or even import real data from your production environment.

If needed, use only short & inline snapshots

When there is a need for snapshot testing, use only short and focused snapshots (i.e. 3-7 lines) that are included as part of the test (Inline Snapshot) and not within external files. Keeping this guideline will ensure your tests remain self-explanatory and less fragile.

On the other hand, ‘classic snapshots’ tutorials and tools encourage to store big files (e.g. component rendering markup (BAD!), API JSON result) over some external medium and ensure each time when the test run to compare the received result with the saved version. This, for example, can implicitly couple our test to 1000 lines with 3000 data values that the test writer never read and reasoned about. Why is this wrong? By doing so, there are 1000 reasons for your test to fail - it’s enough for a single line to change for the snapshot to get invalid and this is likely to happen a lot. How frequently? for every space, comment or minor CSS/HTML change. Not only this, the test name wouldn’t give a clue about the failure as it just checks that 1000 lines didn’t change, also it encourages to the test writer to accept as the desired true a long document he couldn’t inspect and verify. All of these are symptoms of obscure and eager test that is not focused and aims to achieve too much.

For components, the use of snapshots is largely unneeded and overly brittle. A simple change such as wrapping an additional word in a span to get additional styles will cause them to break. As such, snapshot testing should be relegated to extreme edge cases.

Avoid global test fixtures and seeds, add data per-test

Going by the golden rule, each test should add and act on its own set of DB rows to prevent coupling and easily reason about the test flow. In reality, this is often violated by testers who seed the DB with data before running the tests (also known as ‘test fixture’) for the sake of performance improvement. While performance is indeed a valid concern — it can be mitigated, however, test complexity is a much more painful sorrow that should govern other considerations most of the time.

Don’t catch errors, expect them

When trying to assert that some input triggers an error, it might look right to use try-catch-finally and asserts that the catch clause was entered. The result is an awkward and verbose test case that hides the simple test intent and the result expectations.

A more elegant alternative is using the dedicated assertion: expect(method).to.throw or expect(method).toThrow(). It’s mandatory to also ensure the exception contains a property that tells the error type, otherwise given just a generic error the application won’t be able to do much rather than show a disappointing message to the user.

While this is less useful in components, strictly, as errors are usually a state passed in, it does hold for store-based action testing.

Other generic good testing hygiene

  • Learn and practice TDD principles — they are extremely valuable for many but don’t get intimidated if they don’t fit your style, you’re not the only one.
  • Ensure each test checks exactly one thing, when you find a bug — before fixing write a test that will detect this bug in the future.
  • Start a module by writing a quick and simplistic code that satsifies the test.
  • Avoid any dependency on the environment (paths, OS, etc).
  • Don't test things that are already tested (testing a child component when testing a fully mounted component, testing library code that is already well tested).
  • If something is hard to test, or you need to jump through several hoops to test it, chances are it needs to be refactored.

Refs


Other Gist's

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment