Джил Зилберфельд – консультант по внедрению гибких методологий управления проектами. В своем интервью для компании «Точка качества» Джил рассказал, что стоит учесть и чего ожидать при введении модульных тестов в цикл разработки.
Добрый день, Джил. Расскажите немного о себе. Чем вы занимаетесь? Каков ваш профессиональный опыт?
Добрый день и спасибо за интервью. Область программного обеспечения всегда была частью моей жизни.
Более 20 лет я занимался разработкой, тестированием ПО, управлял командами разработчиков и тестировщиков, внедрял и модернизировал практики управления проектами. Кроме этого, я руководил продуктовыми командами, занимался маркетингом, предпродажной подготовкой и сопровождением продуктов.
Сегодня я консультант, помогаю командам разработчиков внедрять новые практики, уточнять требования к продукту, создавать новые стратегии тестирования и выпускать качественный программный продукт с высокой конечной стоимостью. А еще я помогаю внедрять принципы гибких методологий Scrum или Kanban (обычно это нечто среднее, что подходит под запросы отдельно взятой команды).
Вы являетесь автором книги по модульному тестированию Everyday Unit Testing. Расскажите, какую цель вы преследовали при ее написании?
Свою книгу я начал вопросом «Для чего нужна еще одна книга о модульном тестировании?» Сам же на него и отвечу. Я знаком с людьми, которые едва успели разработать тестовый фреймворк, но уже уверены, что знают все о модульном тестировании. В итоге, сталкиваясь с реальными проблемами, для решения которых у них не хватает знаний и опыта, они говорят: «Модульное тестирование – штука хорошая, но здесь она не к месту». Моя книга направлена на предотвращение подобных ситуаций. Она рассчитана не на новичков в тестировании, а на специалистов, знакомых с азами модульного тестирования. Я хотел рассказать им, чего ожидать при работе с унаследованным кодом и как его эффективно тестировать.
Как выбрать подходящую стратегию тестирования?
Однозначного ответа нет. Все зависит от вашего подхода к проблеме, которую предстоит решить. Я начинаю с оценки рисков и задаю себе следующие вопросы:
- Насколько сложна система, и что нам о ней известно?
- Как хорошо разработчики и тестировщики знают доменную область?
- Внедряем ли мы функциональность в изобилующий багами код или имеем дело с абсолютно новым кодом?
Выбор стратегии тестирования будет зависеть от ответов на эти вопросы.
Необходимо также принять во внимание процесс разработки. Если в плане не предусмотрен этап тестирования, то следует учесть риски и защититься от них. В гибких методологиях, сторонником которых я являюсь, тестирование представляет собой итеративный процесс. Такой подход позволяет учесть результаты тестирования текущей версии ПО и внести изменения в дальнейший план.
Далее мы спускаемся до уровня отдельной функциональности или требования. Здесь также стоит сфокусироваться на чем-то конкретном, поскольку мы ограничены во времени. Необходимо определиться, что можно автоматизировать, а где применить исследовательский подход к тестированию.
Не стоит забывать о том, что всегда можно использовать тестовые эвристики. Например, SFDEPOT. С их помощью мы основываем тестирование на предыдущем опыте и определяем области возможных регрессий.
Все вышесказанное применимо к стратегии тестирования в целом. Внутри нее есть место и для модульных тестов. Чем больше у вас модульных тестов, тем больше внимания можно уделить интеграционному, функциональному и исследовательскому тестированию.
С какими проблемами сталкиваются новички в работе с модульными тестами?
На первый взгляд кажется, что модульное тестирование и разработка через тестирование (Test Driven Development — TDD) – это легко. Инструменты и примеры понятны и просты в использовании. Можно рассчитывать как на подробную документацию, так и на профессиональную помощь онлайн-сообщества. Входной барьер кажется очень низким.
Вы начинаете писать тесты для своего кода десятилетней давности. И тут оказывается, что простые примеры не подходят. Сделать код тестируемым можно только переписав его, но это кажется очень рискованным. Создание тестов занимает много времени, а если вы новичок, вряд ли у вас сразу получится достоверный, простой для понимания код.
Другая проблема заключается в отсутствии мгновенной отдачи. Тест должен проработать некоторое время, прежде чем он сможет обнаружить дефект в коде.
На мой взгляд, проблем технического характера нет. Сложнее всего продолжать тратить ресурсы и не видеть результатов. Только самые настойчивые придут к тому, что в итоге их усилия окупятся, и затраты начнут снижаться.
В чем важность модульных тестов в процессе разработке?
Они не важны, пока у вас нет багов.
На самом деле, если у вас есть альтернатива в виде комплекса регрессионных тестов, которые быстро работают, не зависят от веб-сервера и прочего окружения и эффективно обнаруживают проблемные места, – это здорово. Пользуйтесь ими!
Хорошо иметь в своем распоряжении другие виды тестов, но преимущество модульных тестов – в их быстром отклике. В идеале эти тесты должны запускаться на устройстве разработчика перед введением кода.
Как модульное тестирование влияет на процесс разработки?
Сложность в том, что, как я уже говорил, вы сначала будете вкладывать в разработку модульных тестов, а дивиденды начнете получать позже. Унаследованный код будет сопротивляться. Ну и очевидно, что разработка функциональности с тестом занимает примерно вдвое больше времени.
В итоге процесс разработки будет выглядеть так: времени на написание тестов будет уходить меньше, ведь вы станете опытнее в этом деле, код станет более тестируемым. И поскольку тест будет запускаться постоянно, то баги будут обнаруживаться быстрее. При добавлении большего числа тестов, вы начнете больше доверять автотестам, получите меньше багов, а тестировщики получат больше времени на исследовательское тестирование.
Стоит захотеть, и модульные тесты окажут ощутимое влияние на весь процесс.
Что проверяют модульные тесты?
Отдельные модули исходного кода. Это могут быть отдельные методы или комбинация методов. Качественные модульные тесты работают быстро независимо от того, где и кем они запускаются.
Главная их задача – указать на дефектные места в условных конструкциях (if/else), в ответвлениях кода (switch/case) или в комбинациях обоих. С помощью модульных тестов мы можем протестировать также граничные случаи. Но протестировать поведение всего приложения будет затруднительно. Только представьте, что вы запустили тест для поиска ошибки, вызывающей помехи на линии связи. Вы же не будете разрывать сетевое соединение каждый раз, когда решите запустить тест. В этом случае лучше использовать модульные тесты с применением имитаторов (mock-объектов).
Что вы рекомендуете делать с унаследованным кодом?
Переписывать его, конечно! Код будет становиться все сложнее и сложнее, пока вы не решите переписать его полностью.
Профессиональный ответ таков: следует постепенно упростить код. Зачастую разработчики берут небольшие участки кода и дополняют их тестами. Я же сторонник упрощения кодовой базы в конкретный момент времени.
В моем представлении процесс должен выглядеть следующим образом: каждый раз, когда вы работаете над функциональностью или устраняете баг, вы пишете тест. Если тест требует внесения небольших изменений в код, вы их вносите. Если требуются значительные изменения с неясным исходом – принимайте решение и двигайтесь дальше. В конце концов, вы поймете, что большая часть кода может постепенно стать более тестируемой.
И стоит помнить про человеческий фактор: советую больше времени уделять проверке написанного кода и самого теста. Чем больше будет проверок, тем лучше результат.
Как тестировщику подобрать правильный тип теста?
Не думаю, что есть единственно верный вид тестов. Тестировщик (или разработчик, как в нашем случае) должен оценивать риски, доменную область и осознавать, что может пойти не так. Может, тест вообще не нужен?
Пример: код получает указатель, который может быть null, что потенциально может привести к крэшу. Но если вы хорошо знаете код, то понимаете, что в данном случае такой вероятности нет. Так для чего же писать тест? Должен ли разработчик тратить ценное время, пытаясь достичь «нереального» состояния системы?
Мы ограничены во времени для тестирования, поэтому нужно подходить избирательно. Хорошего тестировщика отличает способность применять свои знания и опыт для решения того, на что направить усилия, исходя из стратегии, описанной выше.
Когда стоит следовать принципам TDD?
Трудный вопрос. Сторонники TDD скажут, что в любом коде. Я считаю, это не совсем правильно. Концепция «сначала тест» может быть использована при устранении дефекта или добавлении новой функциональности в существующий код. Применение TDD проще и выгоднее при работе с новым кодом.
Это относится к большинству случаев написания кода. Так почему же не все разработчики руководствуются этими правилами?
И снова проблема не в технике или доступных инструментах. Проблема в отношении. TDD требует дисциплины. Для перехода от красного к зеленому и правильной работы теста от разработчиков требуется дописать небольшой отрезок кода или сначала написать модульный тест, а потом код. Многим разработчикам не хватает такой дисциплины. Хотя процедура может быть установлена в принудительном порядке, это редко происходит на практике. Работая по принципу «скоро релиз, пропустим тестирование», на первое место ставится разработка кода, а качество – на второе. Грустно, но факт.
Какие инструменты обычно используются в модульном тестировании и TDD?
Есть два базовых типа инструментов. Разработчики подбирают их в зависимости от языка и технологии разработки. К первому типу относятся тестовые фреймворки семейства xUnit. Это JUnit для Java, NUnit для .Net, Jasmine для javascript, и GoogleTest для C++. Все они достаточно просты и содержат три элемента в своей структуре:
- Идентификация теста – фреймворк отличает тест от обычной функции. Если пользователь добавляет тест к фреймворку, этот же элемент называется регистрацией теста.
- Выполнение и отчетность – запуск всех (или некоторых) тестов и получение результатов.
- Утверждение API – определение критериев приемлемости для тестов.
Сегодня фреймворки могут обладать более сложной структурой, я указал лишь базовые элементы.
Ко второму типу относятся так называемые инструменты – прототипы (mocking tools). Они необходимы при работе с унаследованным кодом.
Mock-фреймворки используются, чтобы:
- Проверить поведение зависимых объектов, чтобы иметь возможность тестировать код изолированно;
- Протестировать взаимодействие кода с зависимым объектом.
Вот некоторые из mock-фреймворков, которые могут поддержать тестовые фреймворки: Mockito в Java, FakeItEasy в .Net, Sinon.js в javascript и Google Test в C++.
Спасибо, Джил, за ваши идеи. Будем рады пообщаться еще.
Узнать больше о модульном тестировании можно из книги Джила Зилберфельда «Everyday Unit Testing».