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

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

image

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

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

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

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

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

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

Моя цель сегодня: улучшить навыки тестирования. Мы рассмотрим unit тестирование и разработку через тестирование с помощью Jest (инструмент тестирования JavaScript). Конечно, есть другие инструменты тестирования, которые вы можете использовать, например, Mocha и Chai.

Давайте начнем!

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


Когда вы решаете протестировать приложение, вы сталкиваетесь с различными типами тестирования: unit тестирование (модульное), интеграционные тесты и функциональные тесты. Остановимся на модульных тестах.

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

Преимущества модульного тестирования:

  • Делает код стабильнее;
  • Облегчает изменения реализации функции без изменения ее поведения;
  • Документирует ваш код. Вы скоро поймете, для чего.
  • Заставляет вас делать правильно проектировать. Действительно, плохо разработанный код часто сложнее тестировать.

Разработка через тестирование (TDD)

Чтобы понять и использовать разработку через тестирование, запомните эти 2 правила:

  1. Пишите тест, который не проходит, до написания кода.
  2. Затем пишите код, который сможет пройти тест.

Когда мы используем TDD, мы говорим о цикле “red, green, refactor”.

Red: вы пишите провальный тест без написания кода.

Green: пишите простейший код, который сможет пройти тест. Даже если код кажется вам наиглупейшим.

Refactor: рефакторинг кода, если необходим. Не беспокойтесь, если вы поменяете код и ваши юнит-тесты сломаются, если что-то пойдет не так.

Структурирование тестового файла


Jest предоставляет функции для структурирования тестов:

describe: используется для группировки ваших тестов и описания поведения вашей функции/модуля/класса. Он принимает два параметра. Первый — это строка, описывающая вашу группу. Второй — это функция обратного вызова, в которой у вас есть тестовые примеры или hook функции.

it или test: ваш модульный тест. Параметры такие же как и у describe. Должен быть описательным. Именование теста зависит от вас, но можно начинать с «Should».

beforeAll (afterAll): hook функция запускает до (и после) все тесты. Принимает один параметр, который является функцией, которую вы будете запускать до (и после) всех тестов.

beforeEach (afterEach): hook функция, которая выполняется до (и после) каждого теста. Принимает один параметр, который является функцией, которую вы будете запускать до (и после) каждого теста.

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

Вы можете пропустить тест, используя .skip на describe и it: it.skip(...) или describe.skip(...). Используя .skip, вы говорите Jest игнорировать тест или группу.

Вы можете выбрать именно те тесты, которые хотите запустить, используя .only на describe и it: it.only(...) или describe.only(...). Это полезно, если у вас много тестов и вы хотите сосредоточиться только на одном или хотите «отладить» ваши тесты.

Настройка Jest


Чтобы показать вам те функции тестирования, которые мы рассматривали выше, нам нужно настроить Jest. Это очень просто.

Вам нужны только Node.js и npm или Yarn. Убедитесь, что вы используете последнюю версию Node.js, поскольку мы будем использовать ES6. Создайте новый каталог и инициализируйте его.

mkdir test-example && cd test-example
npm init -y
# OR
yarn init -y

-y отвечает “да” на все вопросы npm или yarn. Он должен был создать очень простой файл package.json.

Затем добавьте Jest в среду разработки

yarn add jest --dev

Затем добавьте следующий скрипт в ваш package.json:

"scripts": {
  "test": "jest"
}

yarn test будет запускать ваши тестовые файлы в каталоге. По умолчанию Jest распознает файлы, находящиеся внутри каталога __tests__, или файлы, которые заканчиваются на .spec.js, либо .test.js.

На этом все. Вы готовы писать первый тест.

Matchers (образцы)


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

expect(input).matcher(output)

Jest имеет много образцов, вот самый важный:

toBe: сравнивает strict equality (===).

expect(1 + 1).toBe(2)
let testsAreEssential = true
expect(testAreEssential).toBe(true)

toEqual: сравнивает значения между двумя переменными, массивами или объектами.

let arr = [1, 2]
arr.push(3)
expect(arr).toEqual([1, 2, 3])
let x= 1
x++
expect(x).toEqual(2)

toBeTruthy (toBeFalsy): указывает, истинно ли значение (или ложно).

expect(null).toBeFalsy()
expect(undefined).toBeFalsy()
expect(false).toBeFalsy()
expect("Hello world").toBeTruthy()
expect({foo: 'bar'}).toBeTruthy()

toContain: проверяет, содержит ли массив элемент.

expect(['Apple', 'Banana', 'Strawberry']).toContain('Apple')

toThrow: проверяет, вызывает ли функция ошибку.

function connect () {
  throw new ConnectionError()
}
expect(connect).toThrow(ConnectionError)

Первые тесты


Теперь мы собираемся написать наш первый тест и поиграть с нашими функциями. Сначала создайте в своем каталоге файл example.spec.js и вставьте следующее:

describe('Example', () => {
  beforeAll(() => {
    console.log('running before all tests')
  })
  afterAll(() => {
    console.log('running after all tests')
  })
  beforeEach(() => {
    console.log('running before each test')
  })
  afterEach(() => {
    console.log('running after each test')
  })
  it('Should do something', () => {
    console.log('first test')
  })
  it('Should do something else', () => {
    console.log('second test')
  })
})

Обратите внимание, что нам не нужно импортировать все функции, которые мы используем. Они уже предоставлены Jest.

Запустите yarn test:

image

Поскольку у вас нет утверждений в тестах, они пройдут. Вы видели разные инструкции console.log? Вы должны лучше понимать, как работают ваши функции и тестовые примеры.

Теперь удалите все hook функции и добавьте .skip в первый тест:

describe('Example', () => {
  it.skip('Should do something', () => {
    console.log('first test')
  })
  it('Should do something else', () => {
    console.log('second test')
  })
})

Запустите yarn test еще раз:

image

Ничего страшного, если первый тест работать не будет.

Добавьте третий тест и используйте .only:

describe('Example', () => {
  it('Should do something', () => {
    console.log('first test')
  })
  it('Should do something else', () => {
    console.log('second test')
  })
  it.only('Should do that', () => {
    console.log('third test')
  })
})

Еще раз запустим yarn test:

image

Во второй части статьи мы сделаем простую реализацию стека в JavaScript с помощью TDD.

Комментарии (11)


  1. vintage
    16.11.2018 23:06
    -1

    вы пишите провальный тест без написания кода.

    Это работает для первого теста. Но что делать со вторым, третьим, десятым? Что делать, когда ты не можешь написать ни одного красного теста, так как код сейчас работает верно, но тестовые сценарии покрыты определённо не все?


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

    Как же тут не беспокоиться, если ранее мы не смогли написать красных тестов, оставив не все тестовые сценарии покрытыми?


    1. VolCh
      17.11.2018 23:02

      > Что делать, когда ты не можешь написать ни одного красного теста, так как код сейчас работает верно, но тестовые сценарии покрыты определённо не все?

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


      1. vintage
        17.11.2018 23:34

        Вы ещё раз повторили всем известные мантры, но не ответили ни на один заданный мною вопрос.


        1. VolCh
          18.11.2018 12:37

          Выделяю, то, что вы не заметили:


          Но что делать со вторым, третьим, десятым? Что делать, когда ты не можешь написать ни одного красного теста, так как код сейчас работает верно, но тестовые сценарии покрыты определённо не все?

          Вы можете писать тесты на каждое входное значение

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


          Пишите хоть на каждую комбинацию входных значений тесты, которые ожидаемо будут зелёными, но не называйте это частью TDD и всё. Или ломайте код перед тем как писать ожидаемо зелёный тест, так чтобы все остальные тесты остались зелёными, а новый был красным.


          1. vintage
            18.11.2018 19:23

            Ну, давайте и я выделю для вас то, что вы не заметили:


            Как же тут не беспокоиться, если ранее мы не смогли написать красных тестов, оставив не все тестовые сценарии покрытыми?

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


            1. VolCh
              19.11.2018 00:14

              Мне казалось очевидным, что ответ на второй вопрос: "после ответа на первый, второй вопрос не имеет смысла".


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


              1. vintage
                19.11.2018 00:27

                TDD решает проблему начала написания кода до уверенности в понимании требований

                Каким образом лишь частичное написание тестов даёт уверенность в понимании требований?


                решает проблему низкого покрытия кода тестами

                Каким образом частичное покрытие тестовых сценариев решает проблему низкого покрытия?


                Есть ещё "бесплатные плюшки" у TDD

                О которых вы нам не расскажете?


                1. Fesor
                  19.11.2018 00:45

                  даёт уверенность в понимании требований?

                  "уверенность" понятия субъективное. Суть в том что бы разработчик начал формулировать требования ДО написания кода. Если уверенности в понимании требований нет — пишите больше тестов, есть уверенность в понимании — возможно тестов уже достаточно.


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


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


                  решает проблему низкого покрытия?

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


                  О которых вы нам не расскажете?

                  Не могу говорить за VolCh но… для меня тесты в TDD это та самая "бесплатная" вещь. А главная цель для меня все же в формализации требований и проектировании интерфейсов (ибо тесты это тот же клиентский код). Тесты такие на ранних этапах будут намекать нам о связанности лишней, о том что с декомпозицией что-то не так пошло и т.д.


                  p.s. я не очень понимаю вашу позицию. Вы хотите затеять очередной холивар нужен ли TDD? А зачем? подход не претендует на универсальность а его плюсы, минусы и прочее разжеваны в 5-ти часовом холиваре DDH, Кента Бэка и Фаулера (Is TDD Dead). Есть еще TDD vs DbC с дядей Бобом и Джимом Копленом.


                  Ну либо накиньте что-то конкретное.


                1. VolCh
                  19.11.2018 00:54
                  +1

                  Каким образом лишь частичное написание тестов даёт уверенность в понимании требований?

                  Про частичное написание тестов я ничего не говорил. Есть требование — формулируем его на языке тестов. Все требования (функциональные, конечно) формулируем. Код пишем минимально необходимый для прохождения теста. Причём минимально необходимый не означает минимально физически, в символах кода, а минимально логический. Например, в функция деления для параметров 4 и 2 ловим исключение деления на ноль, а не выбрасываем его в тест. Когда встретим требование, описывающее деление на ноль — тогда обработаем как в требованиях указано.


                  Каким образом частичное покрытие тестовых сценариев решает проблему низкого покрытия?

                  Мы не делаем частичное покрытие тестовых сценариев, у нас даже понятия такого нет. Вы делаете? Зачем? Тестовые сценарии у QA, мы работаем с требованиями и не покрываем их, а переводим на формальный язык тестов, после чего имплементируем.


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


    1. Fesor
      19.11.2018 00:57
      +1

      Что делать, когда ты не можешь написать ни одного красного теста, так как код сейчас работает верно

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


      Правило что нужно убедиться красный ли тест или нет оно как раз об этом. Если внезапно он окажется зеленым — чудно! Вроде даже у Бэка в книге был расписан пример ситуации в которую он попал не проверив что тест уже зеленый (он начал менять код и тесты падали, а потом оказалось что код можно было не менять).


      но тестовые сценарии покрыты определённо не все?

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


      1. VolCh
        19.11.2018 13:52

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

        И даже встречал рекомендацию при уверенности что тест должен быть зелёным (например, проверить, что при делении на ноль исключение бросает, когда точно знаешь что оно бросится), сначала изменить код чтобы ожидаемого поведения не было и тест таки получился красным, а потом вернуть код как был. Тогда будет больше уверенности, что тест вообще что-то тестирует.