Если название этой статьи привлекло ваше внимание, я уверен, что вы, как разработчик внешнего интерфейса или полного стека, хорошо знакомы с чувством того, что вас ошеломляют все мельчайшие детали построения внешнего интерфейса. модульные тесты с помощью vue-test-utils. Либо это ощущение заставляет вас избегать написания тестов целиком, либо каждый раз, когда вы пишете новый набор тестов, вам нужно повторно запускать простейший тестовый пример 50 раз, чтобы правильно настроить тестовый файл с необходимыми моковами и шпионами .

Так было у меня до недавнего времени, когда я решил применить подход TDD в моем следующем большом проекте. Поскольку TDD требует, чтобы вы писали тесты еще до создания соответствующего компонента и его методов, у меня не было другого выбора, кроме как разработать эффективные методы. Ниже я собираюсь представить совокупность этих техник, которые доказали свою ценность после долгого процесса проб и ошибок.

TL:DR

Структура тестовой папки должна точно соответствовать структуре исходного кода

Извлекайте и централизуйте фиктивные объекты и избегайте магических значений в ваших тестах

Используйте фабрику обертки

Упростите свою фабрику, извлекая фиктивные значения в отдельный файл

Давайте начнем медленно и легко. Вот моя первая рекомендация:

Структура тестовой папки должна точно соответствовать структуре исходного кода

  • Вы можете не заметить этого на ранних этапах написания тестовых файлов, но позже, когда вы добавите больше функций к существующему компоненту, вы захотите добавить соответствующие тесты или поддерживать неработающие тесты, связанные с этим файлом. Для быстрого доступа к тестам данного компонента вы должны иметь возможность перемещаться по своей папке тестов так же, как вы перемещаетесь по исходной папке. С помощью этого метода вы также можете выполнить поиск своего проекта по имени файла, начать вводить имя компонента и увидеть версию «.test.js», расположенную по правильному пути.

Допустим, моя папка src выглядит так:

src
| компоненты - ›auth -› Auth.vue
| store - ›modules -› user.js

Моя тестовая папка должна выглядеть примерно так:

test
| components - ›auth -› auth.test.js
| store- ›modules -› user.test.js

Извлеките и централизуйте фиктивные объекты и Избегайте магических значений в ваших тестах

  • Написание модульных тестов означает подготовку множества имитаций для предсказуемой настройки тестов. Однако часто мы понимаем, что некоторые макеты, используемые в каком-то другом файле, необходимы для новых наборов тестов. Или вы можете даже не осознавать этого, даже если это может быть так, потому что, возможно, кто-то другой написал этот макет, а вы об этом не знаете. Ваши тесты быстро заполнятся бесчисленными строками фиктивных данных. Тем не менее, на мой взгляд, тестовые файлы должны читаться как инструкция от начала до конца (Учитывая А, когда вы выполняете B, происходит C). Извлечение этих фиктивных данных в известное место помогает в обоих случаях. Обычно я создаю папку с именем __mocks__ внутри test folder и импортирую фиктивные данные в свои тесты оттуда. При написании нового теста, если вам нужны фиктивные данные, вы сможете зайти в этот каталог и выполнить поиск по некоторым ключевым словам, чтобы увидеть, существуют ли похожие данные или нет. И ваш тестовый файл будет содержать только ядро ​​теста в удобочитаемой форме.
  • Вам также следует избегать тестирования магических значений на основе этих фиктивных данных. Приведу пример того, о чем я говорю:

Представьте, что мы собираемся протестировать метод addToQueue (newElement) и хотим убедиться, что вычисляемое свойство компонента latestItemId становится идентификатором newElement . Мы могли бы просто сделать следующее:

const newTestElement = {id: 5, name: 'element name'};
wrapper.vm.addToQueue(newTestElement);
expect(wrapper.vm.latestItemId).toBe(5);

Однако при таком подходе становится слишком легко сделать ошибку в будущем, поскольку условия тестирования не связаны с ожидаемыми значениями. Если мне случится провести рефакторинг объявления newTestElement, мне нужно будет вручную проверить каждую зависимость с помощью набора тестов. Помимо этого базового примера, это может быть довольно сложно. Ниже вы можете узнать, как я устраняю это осложнение в своих тестах.

const newTestElement = {id: 5, name: 'element name'};
wrapper.vm.addToQueue(newTestElement);
expect(wrapper.vm.latestItemId).toBe(newtestElement.id);

Используйте фабрику обертки

  • Теперь мы подошли к сути статьи. Создавайте тестовые компоненты (оболочки) на централизованной и стандартизированной фабрике вместо того, чтобы кропотливо создавать тестовые компоненты для каждого тестового примера.

Рассмотрим базовый пример теста, представленный в официальной документации vue-test-utils:

import { createLocalVue, shallowMount } from '@vue/test-utils'
import MyPlugin from 'my-plugin'
import Foo from './Foo.vue'

const localVue = createLocalVue()
localVue.use(MyPlugin)
const wrapper = shallowMount(Foo, {
  localVue,
  mocks: { foo: true }
})
expect(wrapper.vm.foo).toBe(true)

const freshWrapper = shallowMount(Foo)
expect(freshWrapper.vm.foo).toBe(false)
  • В соответствии с принципом модульного тестирования, согласно которому каждый тест должен быть независимым и начинаться с чистого листа, оболочка настраивается для каждого теста. В таком простом примере, как этот, это не кажется проблемой, но по мере того, как ваши компоненты становятся все более и более сложными, этот стиль тестирования становится очень сложным в управлении и почти полностью нечитаемым, поскольку десять строк реального тестового кода может быть потеряно среди сотен строк кода настройки теста. Хуже всего то, что во время каждой настройки вы будете настраивать одни и те же макеты, те же реквизиты, одинаковые заглушки и т. Д. (особенно для общих вызовов магазина и службы), даже между разными компоненты.
  • Вот почему я создал специальный файл, который обрабатывает создание оболочки и предоставляет две функции: componentFactory и defaultComponentConfiguration.

- componentFactory - это функция, которая принимает компонент для монтирования и параметры монтирования, а затем возвращает смонтированный тестовый компонент.

export const componentFactory = (component, componentConfiguration) => {
  const localVue = createLocalVue();
  localVue.use(Vuex);

  for (const [key, value] of Object.entries(componentConfiguration.prototypes)) {
    localVue.prototype[key] = value;
  }
  
  const configuration = {
    localVue,
    store: new Vuex.Store(componentConfiguration.store),
    mocks: componentConfiguration.mocks,
    stubs: componentConfiguration.stubs,
    propsData: componentConfiguration.propsData
  }

  return shallowMount(component, configuration);
}

- defaultComponentConfiguration возвращает просто объект, представляющий наиболее общую отправную точку для всех тестовых оболочек. Это соответствует второму аргументу, переданному в функцию componentFactory выше. Это позволяет нам создать тестовый компонент за считанные секунды с параметрами по умолчанию или выборочно изменять конфигурацию в соответствии с нашими потребностями (например, настраивая данные реквизита или вводя шпионов. Я покажу, как это сделать это через секунду).

export const defaultComponentConfiguration = () => { 
    return {
      store: {
        modules: {
          ...
          user: {
            namespaced: true,
            state: {
              ...
            },
            getters: {
            ...
            },
            actions: {
            ...
            }
          }
        }
      },
      mocks: { 
        $t: jest.fn().mockReturnValue('string'),
        $emit: jest.fn(),
        ...
      },
      stubs: ['router-link', 'router-view'],
      propsData: {},
      prototypes: {
        $CONSTANTS: {
           ...
        },
        ...
      }
    }
};
  • А теперь давайте посмотрим, как все это сочетается.

- Тестирование базового компонента без необходимости настройки:

import { componentFactory, defaultComponentConfiguration } from '../../__mocks__/componentFactory’;
import Foo from '@/modules/myModule/components/Foo’;

describe(’Foo’, () => {
    it(’is a Vue instance’, () => {
        const wrapper = componentFactory(Foo, defaultComponentConfiguration());
        expect(wrapper).toBeTruthy();
    });
});

Независимо от сложности этих конфигураций по умолчанию, мой набор тестов настолько чист и удобочитаем, насколько это возможно.

- Тестирование компонента с дополнительными данными props:

import { componentFactory, defaultComponentConfiguration } from '../../__mocks__/componentFactory’;
import Foo from '@/modules/myModule/components/Foo’;
const customComponentConfiguration = defaultComponentConfiguration();
const generateFooComponent () {
    return componentFactory(Foo, customComponentConfiguration);
}
describe(’Foo’, () => {
    beforeEach(() => {
        customComponentConfiguration.propsData.isEmpty = false;          
    });
    it(’is a Vue instance’, () => {
        const wrapper = generateFooComponent();
        expect(wrapper).toBeTruthy();
    });
});

Чтобы настроить нашу сложную конфигурацию настройки, нам нужно только сделать следующее:

1- Извлеките копию конфигураций по умолчанию как customComponentConfiguration
2- Создайте функцию генерации повторно используемых компонентов с помощью generateFooComponent ()
3- Измените пользовательские конфигурации по мере необходимости перед вызовом generateFooComponent ()

- Тестирование компонента путем внедрения шпионов

import { componentFactory, defaultComponentConfiguration } from '../../__mocks__/componentFactory’;
import Foo from '@/modules/myModule/components/Foo’;
const customComponentConfiguration = defaultComponentConfiguration();
const generateFooComponent () {
    return componentFactory(Foo, customComponentConfiguration);
}
describe(’Foo’, () => {
    const checkIfLoggedInSpy = jest.fn();
    beforeEach(() => {
customComponentConfiguration.store.modules.user.actions.checkIfLoggedIn = checkIfLoggedInSpy;          
    });
it(’dispatches checkIfLoggedIn action from user store when it is mounted’, () => {
        const wrapper = generateFooComponent();
        expect(checkIfLoggedInSpy).toHaveBeenCalledTimes(1);
    });
});

Упростите свою фабрику, извлекая фиктивные значения в отдельный файл

  • В заключение я хотел бы отметить, что во время создания конфигураций компонентов по умолчанию те части, которые я пометил многоточием (например, содержимое модулей хранилища и подключаемых модулей), могут стать довольно длинными, что сделает файл конфигурации огромным. Я рекомендую извлечь эти фиктивные значения в отдельный файл, чтобы вы могли более четко увидеть всю свою тестовую конфигурацию. Когда придет время и вам нужно будет добавить новый получатель в один из ваших модулей store, вам не нужно будет искать правильное место в аду круглых скобок. Для этого вы можете просто перейти к соответствующему файлу. И конфигурация вашего компонента всегда будет простой:
export const defaultComponentConfiguration = () => {
return {
      store: {
        modules: {
          ...
          user: {
            namespaced: true,
            state: userStateSpies,
            getters: userGettersSpies,
            actions: userActionsSpies
          }
        }
      }
    }
    ...
};