Оглавление

    Тестирование

    Паттерн AAA

    Если посмотреть на юнит тест, то для большинства можно чётко выделить 3 части кода:

    1. Arrange (настройка) — в этом блоке кода мы настраиваем тестовое окружение тестируемого юнита;
    2. Act — выполнение или вызов тестируемого сценария;
    3. Assert — проверка того, что тестируемый вызов ведёт себя определённым образом.

    Этот паттерн улучшает структуру кода и его читабельность, однако начинать писать тест нужно всегда с элемента Arrange.

    import { mount } from '@vue/test-utils'
    import TodoApp from './TodoApp.vue'
    
    describe('Добавляет новую задачу в список', async () => {
      /**
       * Arrange
       */
      const wrapper = mount(TodoApp)
    
      /**
       * Act
       */
      await wrapper.get('[data-testid="new-todo"]').setValue('New todo')
      await wrapper.get('[data-testid="form"]').trigger('submit')
    
      /**
       * Assert
       */
      expect(wrapper.findAll('[data-testid="todo"]')).toHaveLength(2)
    })
    

    Один кейс на один тест

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

    Один тест проверяет одну вещь.

    Если вы посмотрите на тест и увидите в нём две независимые проверки, то такой тест лучше разделить на два более простых.

    /**
     * 💩 Плохо: в одном тесте множество проверок
     */
    describe('moment.js', () => {
      it('обрабатывает границы дат', () => {
        let date;
    
        date = new MakeMomentJSGreatAgain('1/1/2015');
        date.addDays(30);
        assert.equal('1/31/2015', date);
    
        date = new MakeMomentJSGreatAgain('2/1/2016');
        date.addDays(28);
        assert.equal('02/29/2016', date);
    
        date = new MakeMomentJSGreatAgain('2/1/2015');
        date.addDays(28);
        assert.equal('03/01/2015', date);
      });
    });
    
    /**
     * 😊 Хорошо: тест разбит на несколько более мелких
     */
    describe('moment.js', () => {
      it('корректно добавляет один месяц', () => {
        const date = new MakeMomentJSGreatAgain('1/1/2015');
        date.addDays(30);
        assert.equal('1/31/2015', date);
      });
    
      it('корректно работает в високосный год', () => {
        const date = new MakeMomentJSGreatAgain('2/1/2016');
        date.addDays(28);
        assert.equal('02/29/2016', date);
      });
    
      it('корректно работает в не високосный год', () => {
        const date = new MakeMomentJSGreatAgain('2/1/2015');
        date.addDays(28);
        assert.equal('03/01/2015', date);
      });
    });
    

    Что и как тестировать во Vue компоненте?

    Перед тестированием компонента надо поставить себя на роль пользователя и работать только с той информацией, которая доступна снаружи:

    • HTML
    • emits
    • slots
    • вызовы внешних сущностей (Vuex, API, и т.д.)

    Категорически запрещается лезть во внутренности компонента. Мы должны взаимодействовать с компонентом только через публичный интерфейс: изменять пропсы, доводить компонент до нужного состояния, взаимодействуя с HTML.

    describe('SBtn.vue', () => {
      /**
       * 💩 Плохо: лезем во внутренности.
       * Название computed поля – личное дело компонента
       */
      it('правильно навешивает классы', () => {
        const wrapper = mount(SBtn)
        
        expect(wrapper.vm.classes).toContain(['s-btn'])
      });
    
      /**
       * 😊 Хорошо: работаем с HTML разметкой
       */
      it('правильно навешивает классы', () => {
        const wrapper = mount(SBtn)
    
        expect(wrapper.classes()).toContain('s-btn')
      });
    
      /**
       * 💩 Плохо: эмулируем события и лезем во внутренности
       */
      it('эмитит событие "click" наверх', () => {
        const wrapper = mount(SBtn)
    
        const vm = wrapper.vm
        vm.onClick(event)
    
        const clickEvent = wrapper.emitted().click
    
        expect(clickEvent[0][0]).toEqual(true)
      });
    
      /**
       * 😊 Хорошо: повторяем поведение пользователя
       */
      it('эмитит событие "click" наверх', async () => {
        const wrapper = mount(SBtn)
    
        await wrapper.find('[data-testid="button"]').trigger('click')
        const clickEvent = wrapper.emitted().click
    
        expect(clickEvent[0][0]).toEqual(true)
      });
    });
    

    shallowMount или mount?

    Вместо того чтобы брать что-то одно, мы берём лучшее из двух миров. Для сложных страниц или компонентов, которые имеют много зависимостей, мы используем shallowMount, в остальных случаях mount.

    Если смотреть по проектам, то:

    • @maxus/apps – shallowMount
    • @maxus/modules – shallowMount
    • @maxus/forms – mount
    • @maxus/svztify – mount

    Как искать компоненты?

    Компоненты следует искать по тому, как их искал бы пользователь, настолько, насколько это возможно. Например, если я хочу отправить форму, я буду искать кнопку, у которой есть текст "Отправить", а не data-testid="submit".

    Однако это не значит, что нельзя применять data-testid, ситуации могут быть разные. Атрибут всегда называется data-testid, т.к. он вырезается нашим бандлером в продакшен сборке, чтобы не засорять разметку.

    Поиск по классам запрещён, т.к. это про дизайн.

     describe('SBtn.vue', () => {
       /**
        * 💩 Плохо: ищем по классам, ненадёжно
        */
       it('имеет правильную разметку', () => {
         const wrapper = mount(SBtn)
         
         const el = wrapper.find('.some-selector')
    
         expect(el.html()).toBe('<div data-testid="el">Текст</div>')
       });
    
       /**
        * 😊 Хорошо: ищем по testid
        */
       it('имеет правильную разметку', () => {
         const wrapper = mount(SBtn)
    
         const el = wrapper.find('[data-testid="el"]')
    
         expect(el.html()).toBe('<div data-testid="el">Текст</div>')
       });
    
       /**
        * 😊 Хорошо: ищем по тексту
        */
       it('имеет правильную разметку', () => {
         const wrapper = createWrapper(within(SBtn))
     
         const el = wrapper.getByText('Текст')
     
         expect(el.html()).toBe('<div>Текст</div>')
       });
     });