Разработка через тестирование (TDD) - это процесс, в котором вы пишете тесты перед написанием связанного кода. Сначала вы пишете тест, описывающий ожидаемое поведение, и запускаете его, чтобы убедиться, что он не работает. Затем вы пишете самый простой и тупой код, чтобы пройти тест. Наконец, вы реорганизуете код, чтобы он стал правильным. И вы повторяете все шаги для каждого теста, пока не закончите.

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

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

Модульные тесты критически важны для создания надежного кода. Их игнорирование или спешка увеличивает шансы того, что ваш код в какой-то момент сломается.

Почему TDD для компонентов?

Тестирование компонента может показаться нелогичным. Как мы видели в Модульном тестировании вашего первого компонента Vue.js, это требует умственного сдвига, чтобы сосредоточиться на тестировании компонентов по сравнению с тестированием простых скриптов, зная, что тестировать, и понимая границу между модульными тестами и сквозным. .

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

В этом руководстве мы создадим палитру цветов. Для каждого образца пользователи могут получить доступ к соответствующему цветовому коду в шестнадцатеричном формате, RGB или HSL.

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

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

Прежде, чем мы начнем

В этом руководстве предполагается, что вы уже что-то создавали с помощью Vue.js и написали для него модульные тесты с помощью Vue Test Utils и Jest (или аналогичного средства запуска тестов). Он не будет углубляться в основы, поэтому сначала убедитесь, что вы освоились. Если вы еще не сделали этого, я рекомендую вам пройти Создайте свой первый компонент Vue.js и Модульное тестирование вашего первого компонента Vue.js.

TL; DR: в этом сообщении подробно рассказывается, как и почему. Он разработан, чтобы помочь вам понять каждое решение, стоящее за тестированием реального компонента Vue.js с помощью TDD, и научить принимать дизайнерские решения для ваших будущих проектов. Если вы хотите понять весь мыслительный процесс, читайте дальше. В противном случае вы можете сразу перейти к запоздалым мыслям в конце или посмотреть окончательный код на GitHub.

Запишите свои характеристики

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

Давайте подумаем о различных частях, составляющих наш компонент, и о том, что они должны делать.

Во-первых, у нас есть набор образцов цвета. Мы хотим иметь возможность передавать список настраиваемых цветов и отображать их как образцы в компоненте. Первый должен быть выбран по умолчанию, и конечный пользователь может выбрать новый, щелкнув по нему.

Во-вторых, у нас есть переключатель цветового режима. Конечный пользователь должен иметь возможность переключаться между тремя режимами: шестнадцатеричным (по умолчанию), RGB и HSL.

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

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

Написать тестовый код

Во-первых, вам нужно создать новый проект Vue с Vue CLI. Вы можете проверить Создайте свой первый компонент Vue.js, если вам нужно пошаговое руководство.

В процессе создания шаблонов вручную выберите функции и убедитесь, что вы установили флажок Модульное тестирование. Выберите Jest в качестве решения для тестирования и продолжайте, пока проект не будет создан, зависимости не установлены, и вы не будете готовы к работе.

Нам нужно будет использовать файлы SVG в качестве компонентов, поэтому вам также необходимо установить для них подходящий загрузчик. Установите vue-svg-loader в качестве зависимости разработчика и добавьте для него правило в свой vue.config.js файл.

// vue.config.js
module.exports = {
  chainWebpack: config => {
    const svgRule = config.module.rule('svg')
    svgRule.uses.clear()
    svgRule.use('vue-svg-loader').loader('vue-svg-loader')
  }
}

Этот загрузчик по умолчанию не работает с Jest, что приводит к сбою тестов. Чтобы исправить это, создайте svgTransform.js файл как описано на веб-сайте и отредактируйте свой jest.config.js следующим образом:

// svgTransform.js
const vueJest = require('vue-jest/lib/template-compiler')
module.exports = {
  process(content) {
    const { render } = vueJest({
      content,
      attrs: {
        functional: false
      }
    })
    return `module.exports = { render: ${render} }`
  }
}
// jest.config.js
module.exports = {
  // ...
  transform: {
    // ...
    '.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\\.svg$': '<rootDir>/svgTransform.js'
  },
  // ...
}

Обратите внимание, что мы удалили «svg» из первого регулярного выражения (того, которое преобразуется с помощью jest-transform-stub). Таким образом, мы гарантируем, что svgTransform.js подхватит SVG.

Дополнительно необходимо установить color-convert как зависимость. Нам он понадобится как в нашем коде, так и в наших тестах позже.

Пока не обслуживайте проект. Мы собираемся писать тесты и полагаться на их прохождение или нет, чтобы двигаться дальше. Мы не хотим контролировать, работает ли то, что мы создаем, тестируя его визуально в браузере или отвлекаясь на то, как оно выглядит.

Вместо этого откройте свой проект и создайте новый однофайловый компонент ColorPicker.vue в каталоге src/components/. В tests/unit/ создайте связанный с ним файл спецификации.

<!-- ColorPicker.vue -->
<template>
  <div></div>
</template>
<script>
export default {}
</script>
<style>
</style>
// ColorPicker.spec.js
import { shallowMount } from '@vue/test-utils'
import ColorPicker from '@/components/ColorPicker'
describe('ColorPicker', () => {
  // let's do this!
})

В вашем терминале выполните следующую команду для запуска тестов:

npm run test:unit --watchAll

На данный момент вы должны получить сообщение об ошибке, потому что у вас еще нет тестов. Но не волнуйтесь; мы скоро это исправим 🙂 Обратите внимание на использование флага --watchAll в команде: Jest теперь наблюдает за вашими файлами. Таким образом, вам не придется повторно запускать тест вручную.

TDD проходит в 3 этапа:

  1. Красный: вы пишете тест, описывающий ожидаемое поведение, а затем запускаете его, чтобы убедиться, что он не прошел.
  2. Зеленый: вы пишете максимально простой и тупой код, чтобы пройти тест.
  3. Рефакторинг: вы реорганизуете код, чтобы он стал правильным.

Шаг 1: красный

Пора написать наш первый тест! Начнем с образцов цвета. Для ясности мы обернем все тесты для каждого отдельного элемента в отдельный набор, используя блок describe.

Во-первых, мы хотим убедиться, что компонент отображает каждый цвет, который мы предоставляем, как отдельный образец. Мы бы передали их как свойства в виде массива шестнадцатеричных строк. В компоненте мы отображаем список как неупорядоченный список и назначаем цвет фона через атрибут style.

import { shallowMount } from '@vue/test-utils'
import ColorPicker from '@/components/ColorPicker'
import convert from 'color-convert'
let wrapper = null
const propsData = {
  swatches: ['e3342f', '3490dc', 'f6993f', '38c172', 'fff']
}
beforeEach(() => (wrapper = shallowMount(ColorPicker, { propsData })))
afterEach(() => wrapper.destroy())
describe('ColorPicker', () => {
  describe('Swatches', () => {
    test('displays each color as an individual swatch', () => {
      const swatches = wrapper.findAll('.swatch')
      propsData.swatches.forEach((swatch, index) => {
        expect(swatches.at(index).attributes().style).toBe(
          `background: rgb(${convert.hex.rgb(swatch).join(', ')})`
        )
      })
    })
  })
})

Мы смонтировали наш ColorPicker компонент и написали тест, который ожидает найти элементы с цветом фона, совпадающим с цветами, переданными в качестве свойств. Этот тест обречен на неудачу: в настоящее время у нас ничего нет в ColorPicker.vue. Если вы посмотрите на свой терминал, у вас должна появиться ошибка, говорящая о том, что с нулевым значением нет элемента. Это здорово! Мы только что успешно прошли первый этап TDD.

Шаг 2: зеленый

Наш тест терпит неудачу; мы на правильном пути. Пришло время сделать это. На данный момент мы не очень заинтересованы в написании рабочего или умного кода, все, что мы хотим, - это сделать Jest счастливым. Прямо сейчас Vue Test Utils жалуется на то, что у нас нет элемента с индексом 0.

[vue-test-utils]: no item exists at 0

Самое простое, что мы можем сделать, чтобы эта ошибка исчезла, - это добавить неупорядоченный список с классом swatch в элемент списка.

<template>
  <div class="color-picker">
    <ul class="swatches">
      <li class="swatch"></li>
    </ul>
  </div>
</template>

Jest все еще жалуется, но ошибка изменилась:

Expected value to equal:
  "background: rgb(227, 52, 47);"
Received:
  undefined

Это имеет смысл; у элемента списка нет атрибута style. Самое простое, что мы можем с этим сделать, - это жестко закодировать атрибут style. В конце концов, это не то, чего мы хотим, но нас это пока не беспокоит. Мы хотим, чтобы наш тест стал зеленым.

Таким образом, мы можем жестко закодировать пять элементов списка с ожидаемыми атрибутами стиля:

<ul class="swatches">
  <li class="swatch" style="background: rgb(227, 52, 47);"></li>
  <li class="swatch" style="background: rgb(52, 144, 220);"></li>
  <li class="swatch" style="background: rgb(246, 153, 63);"></li>
  <li class="swatch" style="background: rgb(56, 193, 114);"></li>
  <li class="swatch" style="background: rgb(255, 255, 255);"></li>
</ul>

Теперь тест должен пройти.

Шаг 3. Рефакторинг

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

<template>
  <div class="color-picker">
    <ul class="swatches">
      <li
        :key="index"
        v-for="(swatch, index) in swatches"
        :style="{ background: `#${swatch}` }"
        class="swatch"
      ></li>
    </ul>
  </div>
</template>
<script>
export default {
  props: {
    swatches: {
      type: Array,
      default() {
        return []
      }
    }
  }
}
</script>

При повторном запуске тестов они все равно должны пройти 🥳 Это означает, что мы успешно реорганизовали код, не повлияв на результат. Поздравляем, вы только что завершили свой первый цикл TDD!

А теперь, прежде чем перейти к следующему тесту, давайте немного поразмышляем. Вам может быть интересно:

«Разве это не глупо? Я знал, что тест не удастся. Разве я не трачу время зря на то, чтобы запустить его, затем жестко запрограммировать правильное значение, увидеть тестовый проход, а затем сделать код правильным? Могу я сразу перейти к этапу рефакторинга? »

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

В этом вся идея разработки через тестирование: мы не пишем код, чтобы все работало, мы пишем код, чтобы тесты проходили. Изменяя отношения, мы обеспечиваем надежные тесты с акцентом на результат.

Что мы тестируем?

Еще один вопрос, который может прийти в голову, - как мы решаем, что тестировать. В Модульном тестировании вашего первого компонента Vue.js мы увидели, что мы должны тестировать только общедоступный API нашего компонента, а не внутреннюю реализацию. Строго говоря, это означает, что мы должны учитывать взаимодействия пользователей и изменения свойств.

Но это все? Например, можно ли сломать выходной HTML-код? Или изменить имена классов CSS? Мы уверены, что на них никто не полагается? Что ты не сам?

Тесты должны дать вам уверенность в том, что вы не отправляете сломанное программное обеспечение. То, что люди могут делать с вашей программой, не должно перестать работать так, как они ожидают. Это может означать разные вещи в зависимости от проекта и варианта использования.

Например, если вы создаете эту цветную панель как компонент с открытым исходным кодом, вашими пользователями будут другие разработчики, которые используют ее в своих проектах. Скорее всего, они будут полагаться на предоставленные вами имена классов, чтобы стилизовать компонент по своему вкусу. Имена классов становятся частью вашего общедоступного API, потому что ваши пользователи полагаются на них.

В нашем случае мы не обязательно создаем компонент с открытым исходным кодом, но у нас есть логика представления, которая зависит от конкретных имен классов. Например, для активных образцов важно иметь active имя класса, потому что мы будем полагаться на него для отображения галочки в CSS. Если кто-то случайно изменит это, мы хотим знать об этом.

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

Следующие тесты

Тестирование образцов

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

test('sets the first swatch as the selected one by default', () => {
  const firstSwatch = wrapper.find('.swatch')
  expect(firstSwatch.classes()).toContain('active')
})

Этот тест тоже должен завершиться неудачно, поскольку элементы списка в настоящее время не имеют классов. Мы можем легко выполнить этот проход, добавив класс в первый элемент списка.

<li
  :key="index"
  v-for="(swatch, index) in swatches"
  :style="{ background: `#${swatch}` }"
  class="swatch"
  :class="{ 'active': index === 0 }"
></li>

Теперь тест пройден; однако мы жестко запрограммировали логику в шаблоне. Мы можем реорганизовать это, выполнив экстернализацию индекса, к которому применяется класс. Таким образом, мы сможем изменить это позже.

<template>
  <!-- ... -->
  <li
    :key="index"
    v-for="(swatch, index) in swatches"
    :style="{ background: `#${swatch}` }"
    class="swatch"
    :class="{ active: index === activeSwatch }"
  ></li>
  <!-- ... -->
</template>
export default {
  // ...
  data() {
    return {
      activeSwatch: 0
    }
  }
}

Это, естественно, приводит нас к нашему третьему испытанию. Мы хотим изменять активный образец всякий раз, когда конечный пользователь нажимает на него.

test('makes the swatch active when clicked', () => {
  const targetSwatch = wrapper.findAll('.swatch').at(2)
  targetSwatch.trigger('click')
  expect(targetSwatch.classes()).toContain('active')
})

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

<li
  :key="index"
  v-for="(swatch, index) in swatches"
  :style="{ background: `#${swatch}` }"
  class="swatch"
  :class="{ active: index === activeSwatch }"
  @click="activeSwatch = index"
></li>

Этот код проходит тест и даже не требует рефакторинга. Это удачный побочный эффект от выполнения TDD: иногда процесс приводит либо к написанию новых тестов, которые либо не нуждаются в рефакторах, либо даже проходят сразу.

Активные образцы должны иметь галочку. Мы добавим его сейчас без написания теста: вместо этого мы будем контролировать их видимость с помощью CSS позже. Это нормально, поскольку мы уже проверили, как применяется класс active.

Сначала создайте checkmark.svg файл в src/assets/.

<svg viewBox="0 0 448.8 448.8">
  <polygon points="142.8 323.9 35.7 216.8 0 252.5 142.8 395.3 448.8 89.3 413.1 53.6"/>
</svg>

Затем импортируйте его в компонент.

import CheckIcon from '@/assets/check.svg'
export default {
  // ...
  components: { CheckIcon }
}

Наконец, добавьте его в элементы списка.

<li ... >
  <check-icon />
</li>

Хороший! Теперь мы можем перейти к следующему элементу нашего компонента: цветовой режим.

Тестирование цветового режима

Давайте теперь реализуем переключатель цветового режима. Конечный пользователь должен иметь возможность переключаться между шестнадцатеричным, RGB и HSL. Мы определяем эти режимы внутри компании, но хотим, чтобы они отображались правильно.

Вместо тестирования меток кнопок мы будем полагаться на названия классов. Это делает наш тест более надежным, поскольку мы можем легко определить имя класса как часть контракта нашего компонента. Однако метки кнопок должны иметь возможность изменяться.

Теперь у вас может возникнуть соблазн проверить эти три конкретных режима, но это сделает тест нестабильным. Что, если мы их изменим? Что, если мы добавим один или удалим его? Логика останется прежней, но тест завершится неудачно, что вынудит нас пойти и отредактировать его.

Одним из решений может быть доступ к данным компонента для динамического перебора режимов. Vue Test Utils позволяет нам делать это через свойство vm, но опять же, это тесно связывает наш тест с внутренней реализацией режимов. Если завтра мы решим изменить способ определения режимов, тест сломается.

Другое решение - продолжить тестирование черного ящика и ожидать, что имя класса будет соответствовать заданному шаблону. Нам все равно, что это color-mode-hex, color-mode-hsl или color-mode-xyz, пока оно выглядит так, как мы ожидаем снаружи. Jest позволяет нам делать это с помощью сопоставителей регулярных выражений.

// ...
describe('Color model', () => {
  test('displays each mode as an individual button', () => {
    const buttons = wrapper.findAll('.color-mode')
    buttons.wrappers.forEach(button => {
      expect(button.classes()).toEqual(
        expect.arrayContaining([expect.stringMatching(/color-mode-\w{1,}/)])
      )
    })
  })
})

Здесь мы ожидаем элементы с классом, который следует шаблону «цветовой режим-» + любой символ слова (в ECMAScript любой символ в пределах [a-zA-Z_0-9]). Мы могли бы добавить или удалить любой режим, какой захотим, и тест по-прежнему действовал бы.

Естественно, сейчас тест должен провалиться, так как кнопок с классом color-mode пока нет. Мы можем сделать это, жестко закодировав их в компоненте.

<div class="color-modes">
  <button class="color-mode color-mode-hex"></button>
  <button class="color-mode color-mode-rgb"></button>
  <button class="color-mode color-mode-hsl"></button>
</div>

Теперь мы можем провести рефакторинг этого кода, добавив режимы как частные данные в наш компонент и перебирая их.

<template>
  <!-- ... -->
  <div class="color-modes">
    <button
      :key="index"
      v-for="(mode, index) in colorModes"
      class="color-mode"
      :class="`color-mode-${mode}`"
    >{{ mode }}</button>
  </div>
  <!-- ... -->
</template>
export default {
  // ...
  data() {
    return {
      activeSwatch: 0,
      colorModes: ['hex', 'rgb', 'hsl']
    }
  }
}

Хороший! Давайте двигаться дальше.

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

test('sets the first mode as the selected one by default', () => {
  const firstButton = wrapper.find('.color-mode')
  expect(firstButton.classes()).toContain('active')
})

Мы можем пройти этот тест, вручную добавив класс в первый элемент списка.

<button
  :key="index"
  v-for="(mode, index) in colorModes"
  class="color-mode"
  :class="[{ active: index === 0 }, `color-mode-${mode}`]"
>{{ mode }}</button>

Наконец, мы можем провести рефакторинг, выполнив экстернализацию индекса, к которому применяется класс.

<template>
  <!-- ... -->
  <button
    :key="index"
    v-for="(mode, index) in colorModes"
    class="color-mode"
    :class="[{ active: index === activeMode }, `color-mode-${mode}`]"
  >{{ mode }}</button>
  <!-- ... -->
</template>
export default {
  // ...
  data() {
    return {
      activeSwatch: 0,
      activeMode: 0,
      colorModes: ['hex', 'rgb', 'hsl']
    }
  }
}

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

test('sets the color mode button as active when clicked', () => {
  const targetButton = wrapper.findAll('.color-mode').at(2)
  targetButton.trigger('click')
  expect(targetButton.classes()).toContain('active')
})

Теперь мы можем добавить директиву @click, как мы это делали с образцами, и сделать тест зеленым без необходимости рефакторинга.

<button
  :key="index"
  v-for="(mode, index) in colorModes"
  class="color-mode"
  :class="[{ active: index === activeMode }, `color-mode-${mode}`]"
  @click="activeMode = index"
>{{ mode }}</button>

Проверка цветового кода

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

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

Начнем с (неудачного) теста.

describe('Color code', () => {
  test('displays the default swatch in the default mode', () => {
    expect(wrapper.find('.color-code').text()).toEqual('#e3342f')
  })
})

Теперь давайте сделаем это, жестко закодировав ожидаемый результат в компоненте.

<div class="color-code">#e3342f</div>

Хороший! Пришло время рефакторинга. У нас есть необработанный цвет в шестнадцатеричном формате, и мы готовы выводить его в шестнадцатеричном формате. Единственная разница между нашими входными и выходными значениями состоит в том, что мы хотим добавить к последним символ решетки. Самый простой способ сделать это с помощью Vue - использовать свойство computed.

<template>
  <!-- ... -->
  <div class="color-code">{{ activeCode }}</div>
  <!-- ... -->
</template>
export default {
  // ...
  computed: {
    activeCode() {
      return `#${this.swatches[this.activeSwatch]}`
    }
  }
}

Это должно сохранить тестовый зеленый цвет. Однако есть проблема с этим вычисляемым свойством: оно работает только для шестнадцатеричных значений. Он должен продолжать работать, когда мы меняем цвет, но не когда мы меняем режим. Мы можем проверить это с помощью другого теста.

test('displays the code in the right mode when changing mode', () => {
  wrapper.find('.color-mode-hsl').trigger('click')
  expect(wrapper.find('.color-code').text()).toEqual('2°, 76%, 54%')
})

Здесь мы перешли в режим HSL, но по-прежнему получаем шестнадцатеричный вывод. Нам нужно реорганизовать наш код, чтобы наше вычисляемое свойство activeCode знало не только текущий цвет, но и текущий цветовой режим. Один из способов добиться этого - создать вычисленные свойства для каждого режима и проксировать их через activeCode на основе выбранного режима.

Во-первых, мы должны упростить доступ к текущему цвету и режиму. Прямо сейчас нам нужно выполнить поиск по массиву, который повторяется и затрудняет чтение кода. Мы можем использовать вычисленные свойства, чтобы обернуть эту логику.

export default {
  // ...
  computed: {
    // ...
    activeColorValue() {
      return this.swatches[this.activeSwatch]
    },
    activeModeValue() {
      return this.colorModes[this.activeMode]
    }
  }
}

Как видите, мы не пишем тесты для этих вычисленных свойств, поскольку они не являются частью нашего общедоступного API. Мы будем использовать их позже в наших вычисляемых свойствах специального цветового режима, которые сами будут проксироваться в activeCode, который мы тестируем в нашем наборе «Цветовой код». Все, о чем мы заботимся, - это чтобы цветовой код отображался должным образом, чтобы пользователь мог полагаться на них. Как мы добираемся до этого, есть детали реализации, которые мы должны при необходимости изменить.

Теперь мы можем написать наши специальные вычисляемые свойства для каждого режима. Мы сопоставим их имена с именами в colorModes, чтобы мы могли выполнить поиск в массиве позже в activeCode, чтобы вернуть нужное.

Для шестнадцатеричного вывода мы можем экстернализовать то, что сейчас есть в activeCode, и реорганизовать его с помощью activeColorValue.

export default {
  // ...
  computed: {
    // ...
    hex() {
      return `#${this.activeColorValue}`
    }
  }
}

Теперь давайте изменим activeCode, чтобы он проксировал правильное вычисленное свойство в зависимости от активного режима.

export default {
  // ...
  computed: {
    // ...
    activeCode() {
      return this[this.activeModeValue]
    }
  }
}

Это по-прежнему не должно пройти наш последний тест, поскольку мы не написали для него вычисляемое свойство. Однако наш тест, который проверяет, правильно ли отображается режим по умолчанию, все еще проходит, что является хорошим признаком того, что мы на правильном пути.

Теперь мы хотим написать вычисляемое свойство, которое возвращает вывод цвета в режиме HSL. Для этого мы воспользуемся color-convert, пакетом npm, который позволяет преобразовывать цвета во многих различных режимах. Мы уже использовали его в наших тестах, поэтому нам не нужно его переустанавливать.

import convert from 'color-convert'
export default {
  // ...
  computed: {
    // ...
    hsl() {
      const hslColor = convert.hex.hsl(this.activeColorValue)
      return `${hslColor[0]}°, ${hslColor[1]}%, ${hslColor[2]}%`
    }
  }
}

Отлично, наш тест пройден! Теперь мы можем закончить это добавлением отсутствующего режима RGB.

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

  • он не дает компоненту расти каждый раз, когда мы хотим добавить цветовой режим,
  • он разделяет домены: компонент фокусируется на своей собственной логике представления, а утилита цветовых режимов полностью проверяет каждый режим.

Сначала создайте новый файл color.js в каталоге src/utils/ и соответствующий файл спецификации в tests/unit/.

// color.spec.js
import { rgb, hex, hsl } from '@/utils/color'
// color.js
import convert from 'color-convert'
export const rgb = () => {}
export const hex = () => {}
export const hsl = () => {}

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

Для краткости мы рассмотрим все три теста сразу, но процесс останется прежним.

import { rgb, hex, hsl } from '@/utils/color'
const color = 'e3342f'
describe('color', () => {
  test('returns the color into RGB notation', () => {
    expect(rgb(color)).toBe('227, 52, 47')
  })
  test('returns the color into hexadecimal notation', () => {
    expect(hex(color)).toBe('#e3342f')
  })
  test('returns the color into HSL notation', () => {
    expect(hsl(color)).toBe('2°, 76%, 54%')
  })
})

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

export const rgb = () => '227, 52, 47'
export const hex = () => '#e3342f'
export const hsl = () => '2°, 76%, 54%'

Теперь мы можем начать рефакторинг, перенеся код из нашего компонента Vue.

export const hex = () => `#${color}`
export const hsl = color => {
  const hslColor = convert.hex.hsl(color)
  return `${hslColor[0]}°, ${hslColor[1]}%, ${hslColor[2]}%`
}

Наконец, мы можем реализовать нашу rgb функцию.

export const rgb = color => convert.hex.rgb(color).join(', ')

Все тесты должны оставаться зелеными!

Теперь мы можем использовать color утилиты в нашем компоненте Vue и немного его реорганизовать. Нам больше не нужно импортировать color-convert в компонент, и нам не нужны выделенные вычисляемые свойства для каждого режима или даже для получения значений активного цвета и режима. Все, что нам нужно сохранить, это activeCode, где мы можем хранить всю необходимую логику.

Это хороший пример того, как нам помогает тестирование черного ящика: мы сосредоточились на тестировании общедоступного API; таким образом, мы можем реорганизовать внутреннее устройство нашего компонента, не нарушая тестов. Удаление таких свойств, как activeColorValue или hex, не имеет значения, потому что мы никогда не тестировали их напрямую.

// ...
import { rgb, hex, hsl } from '@/utils/color'
const modes = { rgb, hex, hsl }
export default {
  // ...
  computed: {
    activeCode() {
      const activeColor = this.swatches[this.activeSwatch]
      const activeMode = this.colorModes[this.activeMode]
      return modes[activeMode](activeColor)
    }
  }
}

Теперь у нас есть более сжатый код в нашем компоненте и лучшее разделение доменов, но при этом соблюдается контракт компонента.

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

test('displays the code in the right color when changing color', () => {
  wrapper
    .findAll('.swatch')
    .at(2)
    .trigger('click')
  expect(wrapper.find('.color-code').text()).toEqual('#f6993f')
})

Готово! Мы только что создали полнофункциональный компонент Vue с использованием TDD, не полагаясь на вывод браузера, и наши тесты готовы.

Визуальный контроль

Теперь, когда наш компонент готов, мы можем посмотреть, как он выглядит, и поиграть с ним в браузере. Это позволяет нам добавить CSS и убедиться, что мы ничего не пропустили.

Сначала смонтируйте компонент в главный App.vue файл.

<!-- App.vue -->
<template>
  <div id="app">
    <color-picker :swatches="['e3342f', '3490dc', 'f6993f', '38c172', 'fff']"/>
  </div>
</template>
<script>
import ColorPicker from '@/components/ColorPicker'
export default {
  name: 'app',
  components: {
    ColorPicker
  }
}
</script>

Затем запустите приложение, выполнив следующий сценарий, и откройте его в браузере по адресу http://localhost:8080/.

npm run serve

Вы должны увидеть палитру цветов! На данный момент это не так много, но работает. Попробуйте щелкнуть по цветам и изменить цветовой режим; вы должны увидеть изменение цветового кода.

Чтобы увидеть компонент с правильным стилем, добавьте следующий CSS между тегами style:

.color-picker {
  background-color: #fff;
  border: 1px solid #dae4e9;
  border-radius: 0.125rem;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1);
  color: #596a73;
  font-family: BlinkMacSystemFont, Helvetica Neue, sans-serif;
  padding: 1rem;
}
.swatches {
  color: #fff;
  display: flex;
  flex-wrap: wrap;
  list-style: none;
  margin: -0.25rem -0.25rem 0.75rem;
  padding: 0;
}
.swatch {
  border-radius: 0.125rem;
  cursor: pointer;
  height: 2rem;
  margin: 0.25rem;
  position: relative;
  width: 2rem;
}
.swatch::after {
  border-radius: 0.125rem;
  bottom: 0;
  box-shadow: inset 0 0 0 1px #dae4e9;
  content: '';
  display: block;
  left: 0;
  mix-blend-mode: multiply;
  position: absolute;
  right: 0;
  top: 0;
}
.swatch svg {
  display: none;
  color: #fff;
  fill: currentColor;
  margin: 0.5rem;
}
.swatch.active svg {
  display: block;
}
.color-modes {
  display: flex;
  font-size: 1rem;
  letter-spacing: 0.05rem;
  margin: 0 -0.25rem 0.75rem;
}
.color-mode {
  background: none;
  border: none;
  color: #9babb4;
  cursor: pointer;
  display: block;
  font-weight: 700;
  margin: 0 0.25rem;
  padding: 0;
  text-transform: uppercase;
}
.color-mode.active {
  color: #364349;
}
.color-code {
  border: 1px solid #dae4e9;
  border-radius: 0.125rem;
  color: #364349;
  text-transform: uppercase;
  padding: 0.75rem;
}

Вы должны увидеть что-то вроде этого:

Готово!

Запоздалые мысли

Как мы можем улучшить это?

На данный момент у нас есть надежный набор тестов. Несмотря на то, что у нас нет 100% покрытия, мы можем чувствовать себя уверенно, когда наш компонент выходит на рынок и развивается с течением времени. Однако есть еще пара вещей, которые мы могли бы улучшить, в зависимости от варианта использования.

Во-первых, вы можете заметить, что при нажатии на белый образец галочка не появляется. Это не ошибка, а визуальная проблема: галочка есть, но мы ее не видим, потому что она белая на белом. Вы можете добавить немного логики, чтобы исправить это: когда цвет светлее определенного порога (скажем, 90%), вы можете добавить к образцу класс light. Это позволит вам применить определенный CSS и сделать галочку темной.

К счастью, у вас уже есть все, что вам нужно: пакет color-converter может помочь вам определить, является ли цвет светлым (с помощью утилит HSL), и у вас уже есть вспомогательный модуль color для хранения этой логики и ее изолированного тестирования. Чтобы увидеть, как может выглядеть готовый код, загляните в репозиторий проекта на GitHub.

Мы также могли бы усилить набор, добавив несколько тестов, чтобы убедиться, что там есть какие-то ожидаемые классы. Это не проверяет фактическую логику, но все же было бы особенно полезно, если бы кто-то полагался на эти имена классов для стилизации компонента снаружи. Опять же, все зависит от вашего варианта использования: проверьте, что не должно измениться без вашего ведома, не добавляйте тесты только ради этого.

Что мы узнали?

Из этого эксперимента с TDD можно извлечь несколько уроков. Он дает много общего, но также подчеркивает несколько проблем, о которых нам следует знать.

Во-первых, TDD - это фантастический способ писать надежные тесты, не слишком много и не слишком мало. Вы когда-нибудь заканчивали работу над компонентом, переходили к тестам и думали «с чего мне вообще начать?»? Глядя на готовый код и выясняя, что тестировать, сложно. Заманчиво сделать это быстро, упустить из виду некоторые важные части и получить неполный набор тестов. Или вы можете принять защитный подход и протестировать все, рискуя сосредоточиться на деталях реализации и написании хрупких тестов.

Использование TDD для разработки компонентов пользовательского интерфейса помогает нам сосредоточиться на том, что именно тестировать, определяя перед написанием любой строки кода, является ли это частью контракта или нет.

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

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

Стоит отметить, что TDD гораздо проще выполнять с помощью спецификаций. Когда у вас уже есть четкое представление обо всем, что должен делать компонент, вы можете преобразовать эти спецификации в тесты. Некоторые команды используют такие фреймворки, как ATDD (разработка на основе приемочных испытаний), где участвующие стороны разрабатывают спецификации с точки зрения бизнеса. Окончательные спецификации или приемочные тесты являются идеальной базой для написания тестов после TDD.

С другой стороны, переход с TDD для тестирования компонентов пользовательского интерфейса поначалу может быть трудным и потребовать некоторых предварительных знаний, прежде чем углубляться в него. Для начала, вам необходимо хорошо знать свои библиотеки тестирования, чтобы вы могли писать надежные утверждения. Посмотрите на тест, который мы написали с регулярным выражением: синтаксис не самый простой. Если вы плохо знаете библиотеку, легко написать тест, который не работает по неправильным причинам, что в конечном итоге может помешать всему процессу TDD.

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

Возьмем, к примеру, первый тест нашего пакета: мы тестируем цвета фона. Однако, несмотря на то, что мы передаем шестнадцатеричные цвета, мы ожидаем возврата значений RGB. Это потому, что Jest использует jsdom, реализацию Node.js стандартов DOM и HTML. Если бы мы запускали наши тесты в определенном браузере, у нас могло бы быть другое возвращаемое значение. Это может быть сложно, когда вы тестируете разные движки. Возможно, вам придется искать более продвинутые утилиты преобразования или использовать переменные среды для обработки различных реализаций.

Стоит ли оно того?

Если вы зашли так далеко, то, вероятно, поняли, что TDD требует времени. В самой статье более 6000 слов! Это может быть немного страшно, если вы привыкли к более быстрым циклам разработки, и, вероятно, будет казаться невозможным, если вы часто работаете под давлением. Однако важно развеять миф о том, что TDD каким-то образом удвоит время разработки при небольшой окупаемости инвестиций, потому что это полностью неверно.

TDD требует некоторой практики, и со временем вы станете быстрее. То, что сегодня кажется неуклюжим, завтра станет второй натурой, если вы будете делать это регулярно. Я призываю вас не отказываться от чего-либо, потому что оно новое и неудобное: дайте ему некоторое время, чтобы справедливо его оценить, а затем примите решение.

Во-вторых, время, потраченное на написание кода, основанного на тестировании, - это время, когда вы не тратите время на исправление ошибок.

Исправление ошибок обходится дороже, чем их предотвращение. Если вам когда-либо приходилось исправлять критические производственные ошибки, вы знаете, что это похоже на удержание открытой раны на хирургическом пациенте одной рукой, пытаясь оперировать другой. В пустыне. Ночью, вечером. Швейцарским армейским ножом. Это беспорядок, стресс, неоптимальность и высокие шансы на то, что в процессе вы напутаете что-то еще. Если вы хотите сохранить рассудок и доверие конечных пользователей к вашему программному обеспечению, вы хотите избегать подобных ситуаций любой ценой.

Тесты помогают выявлять ошибки до того, как они попадут в рабочую среду, а TDD помогает писать более качественные тесты. Если вы считаете, что вам следует протестировать свое программное обеспечение, вам следует в первую очередь позаботиться о том, чтобы эти тесты были полезными. В противном случае все это будет пустой тратой времени.

Как и все остальное, я рекомендую вам попробовать TDD, прежде чем отказываться от этой идеи. Если вы постоянно сталкиваетесь с производственными проблемами или думаете, что можете улучшить процесс разработки, то стоит попробовать. Попробуйте в течение ограниченного времени, измерьте влияние и сравните результаты. Вы можете найти метод, который поможет вам выпускать более качественное программное обеспечение, и почувствуете себя более уверенно при нажатии кнопки «Развернуть».