На двух примерах разработчик Node.js Владо Копич показал, насколько неразрывно связаны создание хорошего кода и простота тестирования.
Тестирование — неизбежный краеугольный камень любого проекта, стоящий всех трудов. Это не значит, что процесс сам по себе приносит удовольствие, на самом деле многие программисты вздрагивают при одной мысли о том, чтобы потратить драгоценные часы на тестирование кода, который и так работает. Postman должно быть достаточно, да?
Ну нет. И вы никогда не должны ориентироваться на удобство: пока вы не протестировали и не исправили все возможные аспекты вашего кода, он не может считаться законченным. И более того, выработка хороших привычек тестирования со временем изменит ваш подход к написанию кода. Вы скоро заметите, что вы пишете более красивый и чистый код, потому что у вас в подсознании сидит мысль: “Как я протестирую эту функцию?” Давайте перейдем к делу!
Красота и бойлерплейт
Тестирование требует определенного уровня знакомства с процессом создания кода и лучшими практиками, так как создание чистого кода значительно снижает расходы на тесты. Что это значит? Что ж, рассмотрим довольно распространенный бэкенд-процесс: создание процесса регистрации пользователя для некого API. Просто, да?
Не всегда. Давайте разделим два возможных подхода: один — от менее опытного разработчика, а второй — от человека, умеющего писать открытый для тестирования код.
Программисту А могут не нравиться promise, поэтому он отказывается от обеспечения связности и читаемости и просто используют callback функции для работы со всеми своими асинхронными вызовами. Это очень распространено среди начинающих программистов (виноват, я и сам так делал в самом начале работы). Так как это работает, программист может даже не задумываться об изучении promise — зачем? Оказывается, существует очень хорошая причина (помимо преимуществ обучения и самосовершенствования).
Если вы постоянно используете обратные вызовы, случается “ад обратных вызовов”, что делает тестирование этого кода реальным вызовом! Не говоря уже о том, что ваш код выглядит ужасно уродливо.
Программист А также перегружает свои контроллеры, включая туда большое количество стандартного кода, который легко можно было извлечь в хелпер, поместить в папку utils и запросить его при необходимости. Но он пока этого не знает, поэтому делает, что умеет. Все это приводит к тому, что контроллер сложно читать, сложно понимать и сложно тестировать.
С другой стороны, программист Б знает promise и любит их. Почему бы их не любить? Когда вы правильно соединяете их, они выглядят хорошо, занимают меньше места на экране, в них проще находить ошибки и их проще тестировать.
Итак, программист Б, более опытный и уверенный в своей способности к решению проблем, работает с promise с самого начала, создает красивый и читаемый код, который делает свою работу, и не нагромождает пирамиды из скобок и диагонального кода.
user.save() .then(user => res.status(200).send(user)) .then(() => console.log('User saved')) .catch(error => next(error))
Он также заботится о создании хелперов, улучшая читаемость и возможность к повторному использованию определенных аспектов контроллера.
Что посеешь — то и пожнешь
В конце концов, оба подхода приводят к завершению работы. Давайте посмотрим, как наши программисты справляются с тестированием кода. Мы изучим два случая: юнит-тестирование для хелперов (utils) и компонентное тестирование для самих контроллеров.
Программист А начинает работать над юнит-тестами… и он понимает, что у него почти нет хелперов для тестирования. Так как он не позаботился о том, чтобы разделить фрагменты кода, которые снова можно использовать, на отдельные файлы, многие из “хелперов” включены в контроллер, что приводит к огромному количеству скопированного и вставленного стандартного кода.
Контроллер может выполнять свою работу, но этот подход ухудшает читаемость и приводит к тому, что программисту А нужно тестировать один и тот же код снова и снова.
У программиста Б, с другой стороны, все хелперы организованы в папке utils и могут быть вызваны по необходимости. Этот подход позволяет программисту Б провести юнит-тест для каждого из них один раз, и он знает, что если тест был пройден, то он будет пройден для каждой функции в контроллере. Чисто, быстро и менее затратно.
Время компонентного тестирования!
Программист А уже приступает к компонентному тестированию, потому что его хелпер находится в контроллере, так что в юнит-тестировании мало смысла. Конечно, это значит, что время для тестирования контроллера значительно возрастает из-за уже упомянутых причин. В дополнение к этому, ему нужно иметь дело с неблагодарным занятием тестирования callback-ов, попадая из метафорического ада функций в настоящий ад тестирования.
Давайте посмотрим, как дела у программиста Б. Отлично! Он только что закончил компонентное тестирование для процесса регистрации, который стал гораздо проще, потому что хелперы уже протестированы с самого начала.
Конечно, больше всего времени здесь экономят promise. Вместо того, чтобы постоянно тестировать каждую ошибку для каждой функции обратного вызова, как программист А, программист Б всего лишь должен протестировать каждый promise дважды. Это упрощается процесс, потому что promise показывает ошибки в блоке .catch() в конце каждой цепочки promise, отображая возможный отказ каждого promise над блоком .catch().
Умный код, простые тесты, счастливый разработчик
Можно сказать, что программисту Б гораздо проще тестировать процесс регистрации, а дальнейшая разработка контроллера будет проще с разделенными хелперами и использованием promise для улучшенной читаемости и функциональности кода.
Эти примеры очень субъективны и случайны, и я специально не сказал о некоторых современных функциях вроде async/await из ES8, чтобы объяснить свою позицию максимально просто: размышляя о тестировании, вы неизбежно создадите более качественный код, который будет легко тестировать.
Это как цикл положительной обратной связи: подсознательно вы начнете думать о коде с позиции тестирования во время его создания. Сохраните себе нервы — в будущем вы будете благодарны.