Pull to refresh

Непопулярные аспекты тестирования

IT systems testing *

Непопулярные аспекты тестирования


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

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

Тесты — размножающиеся точки входа

     Сложно поверить, что существует разработчик, который начал задумываться о тестировании, но до этого им никогда не занимался. Скорее всего тесты, которые он писал, были написаны в точке входа в программу (в C,C#,Java это вариации на тему процедуры main), запущены один или более раз до того момента, пока не отработают без падений, и удалены. Этому разработчику удобно думать о том, что тесты это те же точки входа в программу, но только их много, а создавать их просто. Поэтому работающий тест можно не удалять. Кроме того можно до последнего момента вообще избавиться от основной точки входа (то есть если вы разрабатываете консольную программу, то замените её на dll) и использовать только тесты для этой цели. Далее идет мысль, следствием из которой является TDD:

Тест, который никогда не упал — бесполезный тест

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

Тесты — REPL в статически типизированных языках

     Read, Eval, Print, Loop – так расшифровывается REPL. Наверное, во всех динамических языках он существует, например, в Ruby это irb, в общем случае, REPL это консоль, которая тут же выполняет код, который туда ввели. REPL удобен, когда нужно что-то быстро проверить, например, по возрастанию или по убыванию упорядочивает стандартный метод Sort(). Другое его использование предполагает загрузку написанного кода в память и его быстрое тестирование. Зачастую реализации REPL обладает одним хорошим свойством — историю набранных команд можно сохранить в исходный файл, как и результат их выполнения. Это позволяет писать код следующем образом: попробую сделать соединение к серверу google — черт ошибка, а если так — отлично; что там еще надо, а распарсить результат — этот regex должен работать, а протестирую я его на этом наборе данных, что? ошибка?, а если не жадную версию запустить – работает — победа, после этого осталось сохранить контекст, подредактировать его и программа готова.

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

Тесты повышают инертность кода

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

Тесты — типизация в динамических языках

     Для меня тесты открыл человек, который активно пропагандировал руби, а я пытался ему возражать, защищая языки со статической типизацией, ссылаясь на помощь компилятора. Его ответом был — зачем нужна проверка компилятора, если код покрыт тестами. Этим примером можно проиллюстрировать тот факт, что культура тестирования развита больше у программистов на динамических языках. Другое доказательство этого — на запросы «java tdd» и «ruby tdd» google выдает примерно одинаковое число страниц, но на запросах «java» и «ruby» разница существенна.

Тест — теорема, код — доказательство, тестирование — формальная проверка доказательства

     Отчасти это утверждение следствие предыдущего и связано с изоморфизмом Карри-Говарда, но в данную область математики-программирования я еще не углублялся, поэтому это утверждение основано больше на интуиции, чем на строгой теории. Попытаюсь объяснить это утверждение на примере. Пусть программа это эмуляция противопожарной службы лесничества: есть патруль, который 3 раза в день патрулирует некоторый маршрут, есть пожары, которые появляются случайно в разных местах, если пожар попадает в область видимости патруля, то он считается обнаруженным. Следующее утверждение можно считать теоремой (инвариантом программы): по итогам дня необнаруженными пожарами останутся те, и только те пожары, которые находятся в вне зоны действия патруля или те, которые возникли после начала последнего патрулирования. Но с другой стороны это словами описанный тест, проверяющий работы программы. Думаю после этого примера данный аспект тестирования становится ясен.

Коллективная ответственность за код, но личная за тесты

     Работая в команде, мне приходилось часто либо возмущаться, либо слышать возмущения в свой адрес примерно такого содержания: «Какого черта ты трогал мой код?!». На самом деле смысл этой фразы не в том, что кто то обижен за то, что его код изменили, а тем, что изменив код, автор изменения неявно изменил инвариант, который имел ввиду автор кода. Эту ситуацию легко избежать если использовать тестирование — все инварианты, которые есть в коде будут находить воплощение в коде тестов. В этом случае будут раздаваться только вопли «Какого черта ты трогал мой тест?!», от которых можно избавиться, запретив уставом компании изменять чужие тесты. Этот подход позволит добавит гибкости, так как не надо будет согласовывать изменения кода с его автором. Имея ввиду предыдушее утверждение, можно сказать, что такой подход практикуется уже веками — существуют именые теоремы, например, теорема Пифагора, которые имеют множество не именных доказательств.
Tags:
Hubs:
Total votes 47: ↑42 and ↓5 +37
Views 1.1K
Comments Comments 29