Уровень сложности
Средний
Время на прочтение
5 мин
Недостаточное покрытие
Иногда из-за нашей лени, невнимательности или чего-либо ещё получается неполный охват тестами всех важных сценариев, крайних случаев и потенциальных ошибок. Представим, что у нас есть очень простой класс Calculator, который умеет делать сложение:
Порой, когда на работе заставляют чтобы код сопровождался юнит-тестами, может получиться класс, содержащий всего один тест:
Тестирует ли код выше метод Add? Да, тестирует и даже гарантирует правильность сложения чисел 40 и 2. Однако, ни отрицательные числа, ни большие числа, сумма которых выходит за пределы размера int, ни сложение с нулем здесь не проверены.
Как сделать правильно?
Ещё до написания кода желательно вместе с аналитиком/тестировщиком/другими разработчиками обсудить все возможные корнер-кейсы (крайние случаи) и то, как код должен реагировать при встрече с ними.
Переизбыток тестов
В противоположность к первому пункту можно увлечься и насоздавать много излишних тестов для тривиального или простого кода, что приведет к увеличению нагрузки на поддержку и замедлению выполнения тестов.
Совет по составлению списка проверок с другими людьми здесь так же актуален. Плюс, порой можно воспользоваться передачей параметров в тестовый метод, что сокращает размер файла с тестами и их чтение. Еще более продвинутый вариант — использовать генераторы данных, например, Bogus. Ну и самый хардкорный вариант — использование pairwise testing.
Нетестируемый код
После работы в Лаборатории Касперского, где код обкладывался кучами разных тестов, я наиболее явно ощутил весь смысл слова «тестопригодность», когда встретил код, который мне пришлось несколько дней рефакторить, чтобы добавить для него юнит-тесты. Один из примеров как сделать код нетестопригодным — использовать другие классы напрямую.
Ну и соответственно, чтобы сделать его тестопригодным, надо сделать инверсию зависимости, а если по-русски, то заменить зависимость от класса на зависимость от интерфейса:
Не завязываться на конкретные реализации и как можно скорее убирать такие связи, если они есть — станет легче не только писать тесты, но и просто поддерживать код.
Игнорирование или пропуск тестов
Если бы сам не столкнулся с подобным в нескольких компаниях, то мне бы и в голову не пришло, что тесты можно (а порой даже нужно) игнорировать.
Назначить ответственного человека, либо самому следить за тем, чтобы игнорированием тестов не злоупотребляли, а использовали только когда необходимо. Например, когда идет крупномасштабное внедрение изменений — обновление кодстайла, сторонней или своей библиотеки.
Тестирование реализации
Довольно стандартная ловушка для новичков в юнит-тестировании — написать сначала нужный код и потом на основе этого кода писать тесты. Причем тесты пишутся так, чтобы покрыть логику этого написанного кода. Проблема такого подхода — то, что не было написано, не будет и протестировано. Например, если при добавлении в класс Calculator метода Divide мы не учтем в коде проверку деления на ноль, то и при написании тестов по уже существующему коду вероятность написать тест деления на ноль исчезающе мала.
Вспоминаем совет к первым двум пунктам — составлять тест-кейсы ДО написания кода и опираться в них на требуемую бизнес-логику, а не на уже реализованную функциональность. Хотя, сразу оговорюсь, что для написания тестов к уже существующему коду, по которому никаких зафиксированных бизнес-требований нет и никто их не знает/не помнит, подход на основе реализации вполне подходит. Но только для целей регрессионного тестирования.
Хрупкие тесты
Предположим, что мы написали класс А, который реализует необходимую нам бизнес-логику. Затем, в процессе рефакторинга, мы вынесли из класса А два вспомогательных класса — В и С. Нужно ли писать тесты на все три класса? Конечно, это зависит от логики, которая была вынесена во вспомогательные классы и того, будет ли она использоваться где-то ещё помимо класса А, однако, в 99% случаев писать тесты на классы В и С не нужно.
Рискуя набить оскомину, повторюсь, надо тестировать не написанный код, а бизнес-логику, которая реализуется этим кодом.
Отсутствие организации тестов
Видел я и такие проекты, гды пытались внедрить юнит-тесты, однако, все они лежали в корне тестового проекта и было тяжело разобраться есть ли уже нужные тебе тесты или нет. И вместо того, чтобы разбираться в этом бардаке и искать нужный класс, люди просто создавали ещё один, куда писали свои тесты. Хаос в таком случае только увеличивался.
Надо договориться о том, как будут организованы тесты в вашей компании/команде. Один из наиболее простых и распостраненных подходов — полностью копировать структуру основного проекта, добавляя постфикс «Tests». То есть, если был проект CalculationSolution и в нем был путь Calculations/Calculators/, по которому лежал файл Calculator.cs, для которого мы хотим добавить юнит-тесты, то юнит-тесты должны быть в проекте CalculationSolution.Tests по пути Calculations/Calculators/CalculatorTests.cs.
Божественные тесты
Как в процессе программирования может появиться god object — класс, который делает все и вся, так и при написании тестов могут получаться тесты, в которых проверяется не что-то одно, а сразу штук 10 разных аспектов. Да, такая «денормализация» тестов порой имеет место быть в end-to-end, UI или интеграционных тестах в целях экономии ресурсов (в т.ч. времени выполнения), однако, юнит-тесты должны проходить очень быстро и нет смысла усложнять себе разбор упавших тестов ради экономии пары миллисекунд.
Следить за тем, чтобы один тест тестировал только один аспект бизнес-логики.
Недостаточная обработка ошибок
Соблазн протестировать happy path и, возможно, парочку самых простых в тестировании ошибок может привести к тому, что непойманные на этапе автоматизированного тестирования ошибки приведут к проблемам в продакшн среде и цена этой ошибки будет намного выше. Отличие этого пункта от первого в том, что, если в первом пункте было наглядно видно маленькое количество тестов, их практически не было, то здесь тесты уже есть и их даже может быть много, однако, они могут быть направлены на количество, а не на качество.
Опять же, составлять список тестов заранее, плюс, можно добавить в чеклист ревьюера пункт о том, что все тест-кейсы должны быть реализованы.
Смешивание юнит-тестов с другими видами тестов
Как я писал выше, юнит-тесты обычно проходят очень быстро, так как не требуют сложной подготовки, подтягивания зависимостей и прочего. Остальные виды тестов уже несколько более продвинутые и более ресурсоемкие. Поэтому смешивание всех тестов в одну кучу является не очень хорошим вариантом.
Правильным будет разделять мух от котлет. Сделать это можно, например, разнеся тесты по разным проектам или используя идентифицирующие атрибуты. Далее с помощью этих атрибутов можно настроить так, чтобы ни один коммит не попадал ни в одну ветку до тех пор, пока все юнит-тесты, связанные с этим кодом, не пройдут успешно. Также, можно выделить под разные виды тестов разные виртуальные машины и/или стратегии запуска этих тестов.
Статья подготовлена в рамках набора на специализацию C# Developer. Узнать подробнее о специализации.
Лекция № 10
Тестирование и отладка программ
Тестирование – выполнение программы с целью обнаружения ошибок.
Дейкстра: «Никакое тестирование не может подтвердить правильность программы: в лучшем случае, оно может показать только ее ошибочность».
Отладка – локализация и исправление ошибок.
1. Синтаксические Статический контроль и диагностика компилятором и компоновщиком
2. Ошибки выполнения, выявляемые Динамический контроль:
б) несоответствие типов — run-time системы программирования
в) зацикливание — операционной системой – по превы-
шению лимита времени задачи
3. Программа не соответствует специ- Целенаправленное тестирование
4. Спецификация не соответствует Испытания, бета-тестирование
требованиям – ошибка спецификации
Набор ошибок 2-го вида может быть расширен программистом: контроль можно программировать с помощью утверждений (asserts) проверок, вставляемых в код. Это полезно для проверки правдоподобности промежуточных результатов вычислений и допустимости значений фактических параметров подпрограмм.
Тест – это набор контрольных входных данных совместно с ожидаемыми результатами. К входным данным здесь относятся не только конкретные значения ввода, но и события, их последовательность и временные параметры. Ожидаемые результаты берутся из спецификации программы, а на этапе приемо-сдаточных испытаний это – ожидания пользователей.
· В любой нетривиальной программе на любой стадии ее готовности содержатся необнаруженные ошибки
· Продолжительность тестирования – технико-экономическая проблема: компромисс между временем и полнотой. Поэтому нужно возможно меньшее количество хороших тестов с желательными свойствами:
Ø Детективность: тест должен с большой вероятностью обнаруживать возможные ошибки.
Ø Покрывающая способность: один тест должен выявлять как можно больше ошибок.
Ø Воспроизводимость: ошибка должна выявляться независимо от изменяющихся условий (например, от временных соотношений) – это трудно достижимо для время-зависимых программ, реультаты которых часто невоспроизводимы.
Это благие пожелания; для направленного выбора руководствуются критериями выбора тестов. Критерий должен показать, когда некоторое конечное множество тестов достаточно для проверки программы с некоторой полнотой.
Два вида критериев:
§ Функциональные – если тесты составляются исходя из спецификации программы (тестирование черного ящика). Проверяется правильность выполнения программой всех ее заданных функций. Именно этим критериям в основном и следуют при независимом тестировании.
§ Структурные – если тесты составляются исходя из текста программы (тестирование прозрачного ящика). Проверяется правильность работы при прохождении всех участков кода. Эту работу программисты выполняют постоянно в ходе разработки.
Теория тестирования ПО просто и понятно
Время на прочтение
13 мин
Привет, Хабр! Да-да, про тестирование ПО тут уже куча статей. Здесь я просто буду стараться структурировать как можно более полный охват данных из разных источников (чтобы по теории все основное было сразу в одном месте, и новичкам, например, было легче ориентироваться). При этом, чтобы статья не казалась слишком громоздкой, информация будет представлена без излишней детализации, как необходимая и достаточная для прохождения собеседования (согласно моему опыту), рассчитанное на стажеров/джунов (как вариант, эта информация может быть для общего понимания полезна ИТ-рекрутерам, которые проводят первичное собеседование и попутно задают некоторые около-технические вопросы).
ОСНОВНЫЕ ТЕРМИНЫ
Тестирование ПО (Software Testing) — проверка соответствия между реальным и ожидаемым поведением программы, проводится на наборе тестов, который выбирается некоторым образом. Чем занимаются в тестировании:
Основные цели тестирования
Верификация (verification)Валидация (validation)
Соответствие продукта требованиям (спецификации)Соответствие продукта потребностям пользователей
Дефект (баг) — это несоответствие фактического результата выполнения программы ожидаемому результату.
Следует уметь различать, что:
Жизненный цикл бага
НЕКОТОРЫЕ ТЕХНИКИ ТЕСТ-ДИЗАЙНА
Пример таблицы принятия решений
ВИДЫ ТЕСТИРОВАНИЯ
Основные виды тестирования ПО
Классификация по целям
Нефункциональное тестирование (non-functional testing) — тестирование атрибутов компонента или системы, не относящихся к функциональности.Тестирование пользовательского интерфейса (GUI Testing) — проверка интерфейса на соответствие требованиям (размер, шрифт, цвет, consistent behavior).Тестирование удобства использования (Usability Testing) — это метод тестирования, направленный на установление степени удобства использования, обучаемости, понятности и привлекательности для пользователей разрабатываемого продукта в контексте заданных условий. Состоит из: UX — что испытывает пользователь во время использования цифрового продукта, и UI — инструмент, позволяющий осуществлять интеракцию «пользователь — веб-ресурс».Тестирование безопасности (security testing) — это стратегия тестирования, используемая для проверки безопасности системы, а также для анализа рисков, связанных с обеспечением целостного подхода к защите приложения, атак хакеров, вирусов, несанкционированного доступа к конфиденциальным данным.Инсталляционное тестирование (installation testing) направленно на проверку успешной установки и настройки, а также обновления или удаления приложения.Конфигурационное тестирование (Configuration Testing) — специальный вид тестирования, направленный на проверку работы программного обеспечения при различных конфигурациях системы (заявленных платформах, поддерживаемых драйверах, при различных конфигурациях компьютеров и т.д.)Тестирование на отказ и восстановление (Failover and Recovery Testing) проверяет тестируемый продукт с точки зрения способности противостоять и успешно восстанавливаться, т.е. обеспечивать сохранность и целостность данных, после возможных сбоев, возникших в связи с ошибками программного обеспечения, отказами оборудования или проблемами связи (например, отказ сети).Тестирование локализации (localization testing) — проверка адаптации программного обеспечения для определенной аудитории в соответствии с ее культурными особенностями.Тестирование производительности (performance testing) — определение стабильности и потребления ресурсов в условиях различных сценариев использования и нагрузок.Нагрузочное тестирование (load testing) — определение или сбор показателей производительности и времени отклика программно-технической системы или устройства в ответ на внешний запрос с целью установления соответствия требованиям, предъявляемым к данной системе (устройству).Тестирование стабильности или надежности (Stability / Reliability Testing) — это проверка работоспособности приложения при длительном (многочасовом) тестировании со средним уровнем нагрузки.Стрессовое тестирование (Stress Testing) позволяет проверить насколько приложение и система в целом работоспособны в условиях стресса (например, повышение интенсивности выполнения операций до очень высоких значений или аварийное изменение конфигурации сервера) и также оценить способность системы к регенерации, т.е. к возвращению к нормальному состоянию после прекращения воздействия стресса.Объемное тестирование (Volume Testing) — тестирование, которое проводится для получения оценки производительности при увеличении объемов данных в базе данных приложения.Тестирование масштабируемости (scalability testing) — тестирование, которое измеряет производительность сети или системы, когда количество пользовательских запросов увеличивается или уменьшается.
Классификация по позитивности сценария
Классификация по знанию системы
Классификация по исполнителям тестирования
Классификация по уровню тестирования
Подходы к интеграционному тестированиюСнизу вверх (Bottom Up Integration) Все низкоуровневые модули, процедуры или функции собираются воедино и затем тестируются. После чего собирается следующий уровень модулей для проведения интеграционного тестирования. Данный подход считается полезным, если все или практически все модули, разрабатываемого уровня, готовы. Также данный подход помогает определить по результатам тестирования уровень готовности приложения.Сверху вниз (Top Down Integration) Вначале тестируются все высокоуровневые модули, и постепенно один за другим добавляются низкоуровневые. Все модули более низкого уровня симулируются заглушками с аналогичной функциональностью, затем по мере готовности они заменяются реальными активными компонентами.Большой взрыв («Big Bang» Integration) Все или практически все разработанные модули собираются вместе в виде законченной системы или ее основной части, и затем проводится интеграционное тестирование. Такой подход очень хорош для сохранения времени. Однако если тест кейсы и их результаты записаны не верно, то сам процесс интеграции сильно осложнится, что станет преградой для команды тестирования при достижении основной цели интеграционного тестирования.
Классификация по исполнению кода
Классификация по хронологии выполнения
ДОКУМЕНТАЦИЯ
Требования — это спецификация (описание) того, что должно быть реализовано. Требования описывают то, что необходимо реализовать, без детализации технической стороны решения.
Основные атрибуты требований:
Тест план (Test Plan) — документ, описывающий весь объем работ по тестированию:
Основные пункты из которых может состоять тест-план перечислены в стандарте IEEE 829.
Неотъемлемой частью тест-плана является Traceability matrix — Матрица соответствия требований (МСТ) — это таблица, содержащая соответствие функциональных требований (functional requirements) продукта и подготовленных тестовых сценариев (test cases). В заголовках колонок таблицы расположены требования, а в заголовках строк — тестовые сценарии. На пересечении — отметка, означающая, что требование текущей колонки покрыто тестовым сценарием текущей строки. МСТ используется для покрытия продукта тестами.
test case 1++
test case 2++
test case 3+++
Чек-лист (check list) — это документ, описывающий что должно быть протестировано. На сколько детальным будет чек-лист зависит от требований к отчетности, уровня знания продукта сотрудниками и сложности продукта. Чаще всего, в ЧЛ содержатся только действия, без ожидаемого результата. ЧЛ менее формализован, чем тестовый сценарий.
Тестовый сценарий (Test Case) — это документ, в котором содержатся условия, шаги и другие параметры для проверки реализации тестируемой функции или её части.
Атрибуты тест кейса:
Из тестовых сценариев, сгруппированных по некоему признаку (например, тестируемой функциональности), получаются некоторые наборы. Они могут быть как зависящими от последовательности выполнения (результат выполнения предыдущего является предварительным условием для следующего для Test script), так и независимыми (Test suite).
Отчёт о дефекте (Bug Report) — это документ, описывающий ситуацию или последовательность действий приведшую к некорректной работе функциональности.
ШапкаНазвание/тема: Краткое описание (Summary) некорректного поведения, составляется по схеме WWW, т.е. ЧТО ГДЕ КОГДА (при каких условиях)
Назначен на (Assigned To) сотрудника, который будет с ним разбираться
Статус (Status) бага в соответствии с workflow
Компонент приложения (Component): название тестируемой функции или ее части
Информация по сборке, на которой была найдена ошибка: Номер версии (Version), название ветки
Информация об окружении (Environment): ОС + версия, модель девайса (для мобильных устройств) и т.д.
ОписаниеПодробное описание (Description): указывается по необходимости; как правило, сюда вносятся предусловия (PreConditions) или другая дополнительная полезная информация, например, если для воспроизведения бага нужны специальные знания/данные/инструменты
Шаги воспроизведения (Steps to Reproduce), по которым воспроизводится ситуация, приведшая к ошибке
Фактический Результат (Result), полученный после прохождения шагов воспроизведения, часто может быть = теме/краткому описанию (Summary) + расшифровка чего-либо (например, ошибки по коду), если нужно
Ожидаемый результат (Expected Result): который правильный, т.е. описание того, как именно должна работать система в соответствии с требованиями
Прикрепленные файлыВложения (Attachment): файлы с логами, скриншот или видео каст либо их комбинация для прояснения причины ошибки
Спасибо большое всем за фидбэк, благодаря которому материал обновляется и дополняется