Как писать тесты для программ

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

    Что такое тестирование? Место тестирования в разработке программного продукта — здесь идёт речь об основных положениях тестирования ПО. Представлена блок-схема процесса тестирования ПО. Говорится о видах тестирования: ручное и автоматизированное.

Тестирование при помощи библиотеки JUnit для платформы Android. Основной подход к разработке тестов
— рассмотрены основные моменты разработки Unit тестов для приложения, написанного на платформе Android. Разобраны основные инструменты для написания тестов.

Асинхронные задачи в приложении и Android Instrumentation Test. Доступные решения — рассмотрена проблема тестирования асинхронных задач. Предложены для способа решения данной проблемы. Приведены рекомендации по использованию данных методов.

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

Как начать? Сколько нужно писать тестов? На что нужно писать тесты? На что не нужно писать тесты? Стоит ли всегда применять TDD?

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

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

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

Начнем, пожалуй, с самого главного вопроса: зачем нам вообще нужно тестировать?

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

Следующий ключевой тезис не является особенностью процесса тестирования. Задачи можно условно поделить на два типа: они либо завершены, либо нет, а завершенность задач второго типа — это шкала, где 0 — это “ничего не сделано”, а 1 — это сделано на 100%. И при решении таких задач, 100% решение часто оказывается недостижимым из-за сверхвысоких накладных расходов.

Приведу прекрасный пример. Для многих сервисов критично такое понятие как SLA или, проще говоря, доступность сервиса. Например, на хостинговых площадках пишут что-то в духе “мы обеспечиваем доступность 99.9% наших серверов”. Давайте прикинем сколько часов за год хостинг может оказаться недоступен в рамках его SLA: 0.001 * 365 * 24 = 8.7 . В принципе, неплохо.

Предположим, что обеспечение такого уровня доступности обходится компании в 1000$ . А во сколько обойдется компании добавление каждой новой девятки в конце? То есть обеспечение 99.99 , 99.999 и так далее. Насколько мне известно, на таком уровне обеспечения происходит экспоненциальный (взрывной) рост стоимости. Я уже не говорю про то, что 100% доступность является фантастикой.

Этот пример ярко демонстрирует то, что в задачах с плавающим результатом главным принципом является “максимальный результат за минимальные ресурсы”, другими словами, ищется баланс, при котором мы получаем результат, удовлетворяющий стейкхолдеров (заинтересованные лица), за приемлемый бюджет/сроки.

Читайте также:  Как выключить динамики и включить наушники

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

Этот процесс бесконечен. Добиться 100% покрытия очень дорого и, скорее всего, неоправданно (см. пример выше). Кроме того, никакие тесты не дают вам полную гарантию работоспособности.

Кроме количества тестов и их качества на стоимость так же влияет то, какой тип тестов мы используем. Существует множество классификаций видов тестов, таких как “по знанию системы”, “по степени автоматизации”, “по времени проведения тестирования”. На этом этапе нас интересует только одна классификация: “по степени изолированности компонентов”:

  • Модульное тестирование
  • Интеграционное тестирование
  • Системное тестирование (приемочное)

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

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

Так вот, есть только шкала. Чем более простую и мелкую часть системы мы тестируем — тем дешевле тесты, чем более сложную (составную) — тем сложнее. И ваша задача как специалиста — исходить не из того, чтобы соответствовать своим представлениям о видах тестирования, а писать тесты так, чтобы они в идеале покрывали большее число кейсов при небольших затратах. Я уверен, что на этой фразе некоторые разработчики напряглись, потому что в их картине мира нужно обязательно писать изолированные юнит тесты, а приемочные должны писать только тестировщики. Не буду разводить полемику, просто скажу, что бывает по-разному. Есть проекты, в которых процент юнит тестов (в самом жестком понимании) составляет доли процента от всех остальных тестов (как в Хекслете, хе-хе), а есть те, где пишут только приемочные тесты (отдельные тестировщики).

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

Обычно в таких программах не всегда сразу понятно, какой будет архитектура. Многое зависит от того, что будет добавлено в процессе, например, форматы вывода, поддерживаемые форматы входа, обход директорий (рекурсивный), нечеткий поиск и многое другое.

Основной наблюдаемый мной анти-паттерн в разработке подобных библиотек — это тесты на внутренние мелкие компоненты. Те самые юнит тесты. Почему такой подход не продуктивен? Возможно, это и не очевидно, но такое тестирование, хоть и является модульным, но не является дешевым и качественным. Но, как…?

Читайте также:  Как перевести смартфон на русский язык

Мы уже говорили о том, что архитектура проекта еще не известна, и, как правило, внутреннее разделение на файлы/модули/классы/функции, меняется с космической скоростью. В течение часа все может быть переписано несколько раз. Но теперь вместе с кодом нужно постоянно править тесты, что начинает раздражать. Программист начинает сомневаться в том, что они вообще ему нужны, и не редко просто перестает их писать. Другие продолжают мучаться и переписывать их, хотя чаще происходит другое. Написанные тесты начинают вас сковывать и мозг шлет команды “ты потратил время, оставь все как есть”. Постепенно рефакторить становится все сложнее и ленивее. Это очень похоже на ситуацию, когда предприниматель инвестировал деньги в новое направление и даже если бизнес уже тонет, то ему тяжело отказаться, ведь было потрачено столько сил и средств (в экономике это называют sunk cost, — прим. ред.).

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

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

Более низкий уровень это функция, которая принимает на вход путь до файла и подстроку для поиска, а на выходе (не печатает на экран!) отдает готовый результат, так, чтобы осталось только напечатать его. Такой вид тестов обладает самым лучшим балансом “убедиться в том что все работает/стоимость”. Они косвенно затрагивают все используемые внутренности, не зависят от реализации, очень просты в написании и крайне дешевы в поддержке. По мере стабилизации архитектуры можно добавлять тесты более низкого уровня (если становится понятно что сложность системы слишком высока).

Описанная методика особенно хорошо работает в связке с подходом, когда тесты пишутся до кода (вместе с кодом).

Существует миф о том, что тесты нужны только для регресса, то есть для уверенности, что новый код не сломал старый. Это далеко не так. Более того, это следствие написания тестов как таковых. В некоторых ситуациях первостепенная цель написания тестов — это ускорение разработки. Да-да, вы не ослышались, писать тесты до кода/одновременно с кодом, приводит к серьезному ускорению разработки. Чаще всего такие ситуации связаны с тем, что на вход подаются сложные данные, которые как-то трансформируются и прокидываются дальше. Тестировать руками (во время разработки) такой код очень сложно, нужно подготавливать данные, нужно проверять что результат соответствует ожидаемому.

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

Второе серьезное преимущество TDD заключается в том, что при проектировании кода мы начинаем думать не о том, как сейчас клево насоздаем файлов и разнесем по ним функции, создав десятки абстракций, а начнем думать о по-настоящему важных вещах. О том, как будет использоваться моя библиотека. Удивительно, но начать смотреть с такого угла (а этому учат всех стартаперов, customer development во все поля) не просто, все время хочется окунуться в прекрасный мир архитектуры.

Читайте также:  Как отключить безопасный режим на asus zenfone

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

Юнит-тестирование. . что можно проверять, чтобы было полезно, а не тесты ради тестов?

А что в вашем понимании полезно? В юнит-тестах нужно тестировать довольно очевидные (если смотреть пристально в код) контракты. Буквально каждая ветвь каждого if должна быть покрыта. Их польза в том, что они покраснеют, когда эти контракты случайно поменяются.

Функц. тестирование. . Что может быть еще?

Anton Mashletov, В тдд описана методология, как правильно писать тесты, чтоб они имели смысл

Если вы сейчас будете писать тесты как вам захочется — толку от них не будет

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

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

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

Тестирование ради тестирования — потеря времени.

latteo, "Тестирование ради тестирования — потеря времени."
А вот с этим соглашусь. Такое часто встречается и не понятно зачем вообще было время тратить.

Сколько раз видел люди пишут, что чуть только стартовал проект — ты ОБЯЗАН написать тест. Проект без тестов — не проект, а его разработчик — гавно.

Наверное именно поэтому пошла такая тема.

Сколько раз видел люди пишут, что чуть только стартовал проект — ты ОБЯЗАН написать тест. Проект без тестов — не проект, а его разработчик — гавно.

Все зависит от проекта и бюджета.

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

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

Adblock
detector