Модульное тестирование стало проще

Привет, ребята, сегодня мы поговорим о модульном тестировании! : 0

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

Что такое модульное тестирование?

По сути, модульное тестирование - это написание утверждений относительно наименьшего модуля или фрагмента кода в вашей кодовой базе. Почему это важно? Что ж, это определенно не важно, просто чтобы ваш проект мог иметь «100% тестовое покрытие», давайте выбросим это из окна. Это важно, потому что оно обеспечивает долгосрочную стабильность для ваше приложение. Всякий раз, когда что-либо реорганизуется или изменяется, выполнение правильно написанного набора тестов может быстро выявить ошибки до того, как вы зафиксируете код. Не говоря уже о том, что он может служить источником документации для вас и вашей команды.

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

Что я должен тестировать?

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

Что вы узнаете по окончании этой статьи

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

Чем не будет этот блог

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

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

Определение проблемы!

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

  • Node-postgres (решение для базы данных)
  • Jest (среда тестирования Facebook)
  • Синон (насмешливая библиотека)

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

Время для тестирования

Для начала давайте создадим наш тестовый скрипт в нашем файле package.json.

Теперь мы можем приступить к настройке наших файлов. Я собираюсь начать здесь с настройки файла нашей базы данных, который в конечном итоге должен был стать заглушкой в ​​наших тестах. Этот файл здесь просто экспортирует параметр подключения к пулу из библиотеки node-postgres.

Разобравшись с этим, мы можем теперь настроить пользовательский класс.

Тестирование при наличии определенного возвращаемого значения

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

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

Вот некоторые общие вопросы:

  1. Для чего я тестируюсь?
  2. Могу ли я запустить запрос в моем тесте?
  3. Могу ли я заглушить вызов базы данных?
  4. Если я прерываю вызов базы данных, предоставляя ее возвращаемое значение, как я узнаю, что моя логика действительно работает !?

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

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

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

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

Помните, что «новый» - это клей, и ваш класс не имеет ссылки на какую-либо новую переменную, если она не была предоставлена ​​через конструктор.

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

Поскольку в нашем пользовательском классе функция register возвращает обещание, мы заставляем заглушку разрешаться для этого случая. И как мы узнаем, что разрешит функция? Ну, например, мы определили функцию регистрации, которая возвращает копию модели пользователя при успешном вызове. Так что это отличное место для начала. Но как насчет внешнего объекта со свойством rows? Ну, это просто связано с пониманием того, как внешний API, который вы используете, возвращает свои данные. Читая документы, я знаю, что node-postgres возвращает объект со свойством rows, которое является массивом объектов, вместе со свойством rowCount. среди многих других. Но в этом случае мой исходный код зависит только от свойства rows, так что это все, о чем нам нужно беспокоиться в нашем тесте. Зачем загромождать ненужную информацию?

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

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

Тестирование функции с возвращаемым значением void

Перейдем к тестированию функции, возвращающей void. Начнем с определения того, что делает функция.

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

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

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

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

Давайте посмотрим на наши результаты.

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

Подготовка сценария отрицательного тестового примера

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

А теперь еще немного кода!

Очень быстро мы включили новый метод registrationValidation, а в верхней части нашего метода register запускаем оператор if для этого метода проверки. Если это не удается, мы хотим, чтобы была выдана ошибка.

Здесь мы заглушаем метод registrationValidation и заставляем его возвращать false. (Обратите внимание, как мы использовали return, а не разрешает, потому что этот метод не возвращает обещание) И поскольку мы ожидаем этого сценария выдачи ошибки, в блоке catch мы утверждаем, что e не равно нулю. Вот и все!

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

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

Сложный пример - модульное тестирование метода с несколькими вызовами внешнего API

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

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

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

Чтобы быть кратким, мы создали новый тест, чтобы убедиться, что вызывается user.dateTime.toLocaleString, поскольку это важно для работы нашего кода. Затем мы обновили наши старые тесты, включив в них заглушку для этой новой переменной-члена dateTime. Не могли бы вы подумать о других способах проведения тестов для этой новой интеграции?

Поэтому в подзаголовке я использовал фразу «Сложный пример». И действительно, это определенно сложнее, чем то, что было раньше, но кажется ли это более сложным, чем то, что мы уже сделали? Нет, не совсем!

Давайте посмотрим на наши результаты: D

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

Надеюсь, это было полезно, и надеюсь, вам понравилось! Приветствую лучше написанные приложения!

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



Дополнительно

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

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

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

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

Спасибо за прочтение!