Вы, наверное, слышали, как люди раньше говорили о модульном тестировании — что это удивительная вещь, что она уменьшает количество ошибок, что вы должны включать ее во все свои проекты. И возможно, вам было интересно, но вы смотрели на такие библиотеки, как QUnit или Mocha, и чувствовали себя немного напуганными.

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

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

Базовый сценарий

Рассмотрим следующий код:

function fibonacci(index) {
   var first = 0;
   var second = 1;
   var count = 0;
   while(count !== index) {
      second = second + first;
      first = second - first;
      count++;
   }
   return second;
}

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

console.log(fibonacci(0)); //1
console.log(fibonacci(1)); //1
console.log(fibonacci(2)); //2
console.log(fibonacci(3)); //3
console.log(fibonacci(4)); //5
console.log(fibonacci(5)); //8

Вроде все работает правильно!

За исключением зависания на секунду — есть пограничный случай, о котором мы не подумали. Предположим, кто-то дал нам отрицательное число? В этом случае наш цикл while будет работать бесконечно. Мы не заботимся о том, чтобы отрицательный ввод был правильным, но мы определенно должны предотвратить зависание вкладки!

function fibonacci(index) {
   var first = 0;
   var second = 1;
   for(var i=0; i<index; i++) {
      second = second + first;
      first = second - first;
   }
   return second;
}

Отлично, мы убрали все бесконечные циклы! Вот только… теперь нам нужно все перепроверить.

console.log(fibonacci(0)); //1
console.log(fibonacci(1)); //1
console.log(fibonacci(2)); //1 Um... should be 2
console.log(fibonacci(3)); //2
console.log(fibonacci(4)); //3 Oh, I see what the problem is.
console.log(fibonacci(5)); //5

И это хорошо, что мы это сделали, иначе мы бы пропустили незаметную ошибку «отклонение на единицу».

function fibonacci(index) {
   var first = 0;
   var second = 1;
   for(var i=0; i<=index; i++) {
      second = second + first;
      first = second - first;
   }
   return second;
}
console.log(fibonacci(0)); //1
console.log(fibonacci(1)); //1
console.log(fibonacci(2)); //2
console.log(fibonacci(3)); //3 
console.log(fibonacci(4)); //5
console.log(fibonacci(5)); //8

Намного лучше!

Избегайте повторения

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

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

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

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

Начнем с того, почему нам нужно проверять правильность вывода? Почему компьютер не может этого сделать?

var correct = true;
if (fibonacci(0) !== 1) { correct = false; }
if (fibonacci(1) !== 1) { correct = false; }
if (fibonacci(2) !== 2) { correct = false; }
if (fibonacci(3) !== 3) { correct = false; }
if (fibonacci(4) !== 5) { correct = false; }
if (fibonacci(5) !== 8) { correct = false; }
if (correct) {
   console.log('Working correctly!');
} else {
   console.log('Something is broken!');
}

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

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

Что такое модульный тест?

Модульный тест — это автоматическая проверка одной «единицы» кода — обычно одной функции или поведения в вашей программе.

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

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

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

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

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

new Distilled().test('fibanacci', function () {
    this.test('0:1', fibanacci(0) === 1);
    this.test('1:1', fibanacci(1) === 1);
    this.test('2:2', fibanacci(2) === 1);
    this.test('3:3', fibanacci(3) === 1);
    this.test('4:5', fibanacci(3) === 1);
    this.test('5:8', fibanacci(3) === 1);
});

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

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

Что еще они делают?

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

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

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

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

Итак, давайте еще раз пройдемся по самым основным принципам модульного тестирования:

В итоге

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

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