Тестирование важный навык, которым должен обладать каждый разработчик. Тем не менее, некоторые делают это неохотно.
Каждый из нас сталкивался с разработчиком, который утверждал, что тесты бесполезны, они требуют слишком много усилий, да и вообще его код настолько превосходен, что никакие тесты не нужны. Не верьте. Тестирование крайне важно.
Тесты делают ваш код стабильнее и уменьшают количество багов. Вам кажется, что это не так, потому что вы знаете каждый бит вашего кода. Ведь вы же его и написали, зачем еще тесты?
Предположим, вы создаете приложение для погоды. Вы писали код несколько дней или недель, поэтому знаете код от и до.
Теперь представим, что вы закончили работу над приложением и вернетесь к нему через несколько месяцев. Вы не будете помнить каждую деталь старого кода. Вы измените его… и черт возьми… что-то сломалось. Как вы это исправите? Посмотрите на каждый созданный файл и настроите его, чтобы он снова работал? Может и получится. Но изменив этот файл, вы сломаете что-то еще.
Возьмем еще один пример. После нескольких месяцев напряженной работы, вы наконец получили позицию, которую давно хотели! Вы включаетесь в команду и начинаете что-то создавать. Вы работаете с кодом других разработчиков и наоборот. И в один день все ломается. Если команда не интегрировала тесты в свое приложение, я вам не завидую.
Каждая команда должна писать тесты, когда создает программное обеспечение или приложения. Вы же не хотите быть человеком, который не знает, как тестировать?
Да, написание тестов требует времени. Да, сначала сложно. Да, создавать приложения интереснее. Но тесты важны и экономят время, если они правильно реализованы.
Моя цель сегодня: улучшить навыки тестирования. Мы рассмотрим unit тестирование и разработку через тестирование с помощью Jest (инструмент тестирования JavaScript). Конечно, есть другие инструменты тестирования, которые вы можете использовать, например, Mocha и Chai.
Давайте начнем!
Когда вы решаете протестировать приложение, вы сталкиваетесь с различными типами тестирования: unit тестирование (модульное), интеграционные тесты и функциональные тесты. Остановимся на модульных тестах.
Функциональные и интеграционные тесты так же важны, но их сложнее настроить и реализовать, чем модульные тесты. В двух словах, модульный тест состоит из тестирования небольших частей вашего кода: функций, методов классов и т. д. Вы подаете на вход данные и подтверждаете, что получили ожидаемый результат.
Преимущества модульного тестирования:
Разработка через тестирование (TDD)
Чтобы понять и использовать разработку через тестирование, запомните эти 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. Это очень просто.
Вам нужны только Node.js и npm или Yarn. Убедитесь, что вы используете последнюю версию Node.js, поскольку мы будем использовать ES6. Создайте новый каталог и инициализируйте его.
-y отвечает “да” на все вопросы npm или yarn. Он должен был создать очень простой файл package.json.
Затем добавьте Jest в среду разработки
Затем добавьте следующий скрипт в ваш package.json:
yarn test будет запускать ваши тестовые файлы в каталоге. По умолчанию Jest распознает файлы, находящиеся внутри каталога __tests__, или файлы, которые заканчиваются на .spec.js, либо .test.js.
На этом все. Вы готовы писать первый тест.
Когда вы что-то проверяете, вам нужен вход и ожидаемый результат. Вот почему Jest предоставляет образцы для проверки наших значений:
Jest имеет много образцов, вот самый важный:
toBe: сравнивает strict equality (===).
toEqual: сравнивает значения между двумя переменными, массивами или объектами.
toBeTruthy (toBeFalsy): указывает, истинно ли значение (или ложно).
toContain: проверяет, содержит ли массив элемент.
toThrow: проверяет, вызывает ли функция ошибку.
Теперь мы собираемся написать наш первый тест и поиграть с нашими функциями. Сначала создайте в своем каталоге файл example.spec.js и вставьте следующее:
Обратите внимание, что нам не нужно импортировать все функции, которые мы используем. Они уже предоставлены Jest.
Запустите yarn test:
Поскольку у вас нет утверждений в тестах, они пройдут. Вы видели разные инструкции console.log? Вы должны лучше понимать, как работают ваши функции и тестовые примеры.
Теперь удалите все hook функции и добавьте .skip в первый тест:
Запустите yarn test еще раз:
Ничего страшного, если первый тест работать не будет.
Добавьте третий тест и используйте .only:
Еще раз запустим yarn test:
Во второй части статьи мы сделаем простую реализацию стека в JavaScript с помощью TDD.
Каждый из нас сталкивался с разработчиком, который утверждал, что тесты бесполезны, они требуют слишком много усилий, да и вообще его код настолько превосходен, что никакие тесты не нужны. Не верьте. Тестирование крайне важно.
Тесты делают ваш код стабильнее и уменьшают количество багов. Вам кажется, что это не так, потому что вы знаете каждый бит вашего кода. Ведь вы же его и написали, зачем еще тесты?
Предположим, вы создаете приложение для погоды. Вы писали код несколько дней или недель, поэтому знаете код от и до.
Теперь представим, что вы закончили работу над приложением и вернетесь к нему через несколько месяцев. Вы не будете помнить каждую деталь старого кода. Вы измените его… и черт возьми… что-то сломалось. Как вы это исправите? Посмотрите на каждый созданный файл и настроите его, чтобы он снова работал? Может и получится. Но изменив этот файл, вы сломаете что-то еще.
Возьмем еще один пример. После нескольких месяцев напряженной работы, вы наконец получили позицию, которую давно хотели! Вы включаетесь в команду и начинаете что-то создавать. Вы работаете с кодом других разработчиков и наоборот. И в один день все ломается. Если команда не интегрировала тесты в свое приложение, я вам не завидую.
Каждая команда должна писать тесты, когда создает программное обеспечение или приложения. Вы же не хотите быть человеком, который не знает, как тестировать?
Да, написание тестов требует времени. Да, сначала сложно. Да, создавать приложения интереснее. Но тесты важны и экономят время, если они правильно реализованы.
Моя цель сегодня: улучшить навыки тестирования. Мы рассмотрим unit тестирование и разработку через тестирование с помощью Jest (инструмент тестирования JavaScript). Конечно, есть другие инструменты тестирования, которые вы можете использовать, например, Mocha и Chai.
Давайте начнем!
Unit тестирование
Когда вы решаете протестировать приложение, вы сталкиваетесь с различными типами тестирования: unit тестирование (модульное), интеграционные тесты и функциональные тесты. Остановимся на модульных тестах.
Функциональные и интеграционные тесты так же важны, но их сложнее настроить и реализовать, чем модульные тесты. В двух словах, модульный тест состоит из тестирования небольших частей вашего кода: функций, методов классов и т. д. Вы подаете на вход данные и подтверждаете, что получили ожидаемый результат.
Преимущества модульного тестирования:
- Делает код стабильнее;
- Облегчает изменения реализации функции без изменения ее поведения;
- Документирует ваш код. Вы скоро поймете, для чего.
- Заставляет вас делать правильно проектировать. Действительно, плохо разработанный код часто сложнее тестировать.
Разработка через тестирование (TDD)
Чтобы понять и использовать разработку через тестирование, запомните эти 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:
Поскольку у вас нет утверждений в тестах, они пройдут. Вы видели разные инструкции 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 еще раз:
Ничего страшного, если первый тест работать не будет.
Добавьте третий тест и используйте .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:
Во второй части статьи мы сделаем простую реализацию стека в JavaScript с помощью TDD.
vintage
Это работает для первого теста. Но что делать со вторым, третьим, десятым? Что делать, когда ты не можешь написать ни одного красного теста, так как код сейчас работает верно, но тестовые сценарии покрыты определённо не все?
Как же тут не беспокоиться, если ранее мы не смогли написать красных тестов, оставив не все тестовые сценарии покрытыми?
VolCh
> Что делать, когда ты не можешь написать ни одного красного теста, так как код сейчас работает верно, но тестовые сценарии покрыты определённо не все?
TDD, как ни странно, ни о полном покрытии кода тестовыми сценариями. Оно о том, если грубо, что для изменения поведения кода должна быть только одна причина — минимум один упавший тест. Вы можете писать тесты на каждое входное значение, вы можете рефакторить и оптимизировать, но не можете изменять логику, если нет хотя бы одного красного теста. А можете даже и не рефакторить, не говоря о тестовых сценариях.
vintage
Вы ещё раз повторили всем известные мантры, но не ответили ни на один заданный мною вопрос.
VolCh
Выделяю, то, что вы не заметили:
Ещё подробнее надо? TDD не о тестировании всех классов эквивалентности или всех возможных значений. TDD не допускает написания ожидаемо зелёных тестов в рамках реализации поведения модуля, но не запрещает их писать вне этого процесса.
Пишите хоть на каждую комбинацию входных значений тесты, которые ожидаемо будут зелёными, но не называйте это частью TDD и всё. Или ломайте код перед тем как писать ожидаемо зелёный тест, так чтобы все остальные тесты остались зелёными, а новый был красным.
vintage
Ну, давайте и я выделю для вас то, что вы не заметили:
А подробней хотелось бы не рассказы о том, что ТДД разрешает или запрещает, и что можно называть ТДД, а что нельзя. Расскажите лучше какие проблемы ТДД решает и обоснуйте каким образом это следует из накладываемых ТДД ограничений.
VolCh
Мне казалось очевидным, что ответ на второй вопрос: "после ответа на первый, второй вопрос не имеет смысла".
TDD решает проблему начала написания кода до уверенности в понимании требований, а также решает проблему низкого покрытия кода тестами, причём не формального, а реального. TDD не единственный инструмент для этого, но работающий. Есть ещё "бесплатные плюшки" у TDD
vintage
Каким образом лишь частичное написание тестов даёт уверенность в понимании требований?
Каким образом частичное покрытие тестовых сценариев решает проблему низкого покрытия?
О которых вы нам не расскажете?
Fesor
"уверенность" понятия субъективное. Суть в том что бы разработчик начал формулировать требования ДО написания кода. Если уверенности в понимании требований нет — пишите больше тестов, есть уверенность в понимании — возможно тестов уже достаточно.
У Кента Бэка в его книге про разработку через тестирование было об этом, мол длина итераций красный-зеленый-рефакторинг зависит от вашей уверенности. Ну и в целом он в своей книге пишет что это все способ борьбы со страхом внесения изменений.
Опять же, способ этот не универсален и он хорошо работает на ком-то вроде Кента Бэка, но может плохо работать для кого-нибудь другого. Ну и не для всех задач такой способ подойдет. Универсальных вещей в этом плане как бы нету.
Если для того что бы написать строчку кода у вас должен быть красный тест, значит тест станет зеленым, и стало быть низким покрытие кода у нас быть уже не может.
Не могу говорить за VolCh но… для меня тесты в TDD это та самая "бесплатная" вещь. А главная цель для меня все же в формализации требований и проектировании интерфейсов (ибо тесты это тот же клиентский код). Тесты такие на ранних этапах будут намекать нам о связанности лишней, о том что с декомпозицией что-то не так пошло и т.д.
p.s. я не очень понимаю вашу позицию. Вы хотите затеять очередной холивар нужен ли TDD? А зачем? подход не претендует на универсальность а его плюсы, минусы и прочее разжеваны в 5-ти часовом холиваре DDH, Кента Бэка и Фаулера (Is TDD Dead). Есть еще TDD vs DbC с дядей Бобом и Джимом Копленом.
Ну либо накиньте что-то конкретное.
VolCh
Про частичное написание тестов я ничего не говорил. Есть требование — формулируем его на языке тестов. Все требования (функциональные, конечно) формулируем. Код пишем минимально необходимый для прохождения теста. Причём минимально необходимый не означает минимально физически, в символах кода, а минимально логический. Например, в функция деления для параметров 4 и 2 ловим исключение деления на ноль, а не выбрасываем его в тест. Когда встретим требование, описывающее деление на ноль — тогда обработаем как в требованиях указано.
Мы не делаем частичное покрытие тестовых сценариев, у нас даже понятия такого нет. Вы делаете? Зачем? Тестовые сценарии у QA, мы работаем с требованиями и не покрываем их, а переводим на формальный язык тестов, после чего имплементируем.
TDD способствует низкой связанности и высокой связности, TDD улучшает документирование, TDD статистически снижает время на отладку
Fesor
Напишите зеленый тест, ограничение же действует на реализацию а не на тесты. Единственная причина по которой вы можете захотеть добавить новый тест — вы хотите закрепить еще одно требование. В этом случае не важно, реализовано оно уже или нет. Важно что бы вы не добавлялии в реализацию вещей для которых требования в тестах не закреплены.
Правило что нужно убедиться красный ли тест или нет оно как раз об этом. Если внезапно он окажется зеленым — чудно! Вроде даже у Бэка в книге был расписан пример ситуации в которую он попал не проверив что тест уже зеленый (он начал менять код и тесты падали, а потом оказалось что код можно было не менять).
добавлять больше зеленых тестов вам никто не запрещает. Просто это будет уже увеличение покрытия не влияющее на дизайн кода. Опять же именно по этому принципу построена "триангуляция".
VolCh
Не помню где читал рекомендацию, что при сомнениях в том, что новый тест должен быть зелёным очень серьёзно надо проверять и код, и тест на ошибки. Ошибки гораздо чаще встречаются чем чудеса :)
И даже встречал рекомендацию при уверенности что тест должен быть зелёным (например, проверить, что при делении на ноль исключение бросает, когда точно знаешь что оно бросится), сначала изменить код чтобы ожидаемого поведения не было и тест таки получился красным, а потом вернуть код как был. Тогда будет больше уверенности, что тест вообще что-то тестирует.