Оригинальная публикация: https://habrahabr.ru/company/jugru/blog/329372/
Тестирование программного кода — кропотливый и сложный процесс. Львиную долю работы в нем совершают unit-тесты. Пока они не «загорятся зеленым», тестировать дальше смысла нет. Как же писать unit-тесты правильно? Стоит ли гнаться за 100% покрытием? С какими сложностями приходится сталкиваться инженерам на практике? Своим опытом делятся Marc Philipp и Всеволод Брекелов.
Marc Philipp – один из основных разработчиков фреймворка JUnit 5 – инструмента для Java-тестировщиков. В данный момент работает в качестве инженера в немецкой компании LogMeIn над облачными SaaS-решениями.
Всеволод Брекелов — Senior QA Engineer в компании Grid Dynamics, более 5 лет занимается тестированием, имеет опыт построения автоматизации тестирования с нуля.
— В статьях про unit-тестирование в качестве примеров обычно приводят тестирование методов и классов калькулятора. Такие примеры могут показать сложность реальных задач? С чем приходится сталкиваться тестировщику полнофункциональных программ? Marc Philipp: Действительно, на примерах с калькулятором невозможно показать сложность реальных задач. Они выбраны в статьях для того, чтобы читатели могли сосредоточиться на понимании подходов unit-тестирования без необходимости разбора сложного кода. Хотя эти примеры очень простые, они хорошо демонстрируют основную идею и принципы unit-тестирования. В реальной жизни тестируемый код должен быть изначально написан с учетом того, что по нему будет проводиться Unit-тестирование. Один из способов обеспечить это — писать тесты до написания кода или практически одновременно с ним. Когда у вас есть код, адаптированный к тестированию, написание unit-тестов не на много сложнее, чем для калькулятора. Всеволод Брекелов: Думаю, что сложность реальных задач можно понять только на реальных задачах. Если серьезно, то есть и хорошие статьи, где весьма подробно рассматриваются нетривиальные примеры. Думаю, что они помогут приблизиться в реальности. К примеру, по запросу «unit тестирование java» можно быстро найти статью на Хабре. Она опубликована довольно давно, но не потеряла своей актуальности. Что касается особенностей работы, я бы выделил следующие группы тестировщиков (надеюсь никого не обидеть):
- Back-end – могут писать системные, интеграционные, компонентные, юнит тесты (почему бы и нет?).
- Front-end – могут писать как e2e тесты, так и компонентные, так и юнит тесты.
- DB — занимаются тестированием данных/самой БД.
- Performance – тут вроде бы очевидно.
- Infrastructure – занимаются больше вопросами «около-девопсными».
- Mobile testing(iOS, Androind, IoT) — сейчас очень модно стало отделять таких инженеров, хотя на мой взгляд это все о том же Back-end/Front-end.
Эти люди обычно занимаются не только тестированием самого программного обеспечения, но и требований, и самого процесса. Правда, подходят к этому чаще всего формально, что, на мой взгляд, неправильно. Хотел бы обратить внимание на процесс. Я считаю, что каждый тестировщик должен хорошо разбираться в построении процесса разработки, так как в моей практике ноги, баги и основная трата времени на имплементацию того, что не нужно, растут как раз оттуда. — Каждый тест должен проверять одну вещь. Насколько полно на практике удается выполнить это условие? Как вы боретесь с зависимостями, какие фреймворки используете? Marc Philipp: При написании unit-тестов обычно берется один образец входных данных из класса эквивалентности в тестируемой проблемной области. Конечно, вы должны сначала определить эти самые классы эквивалентности. В каждом тесте вы добавляете assertion только для тех свойств, которые релевантны вашему тесту. Не следует копипастить одни и те же assertions в каждый новый тест и прогонять их. Когда у вас есть зависимости, влияющие на работу юнита, подумайте об использовании стабов или моков, чтобы сохранить независимость теста. Многие наши юнит-тесты для JUnit 5 используют моки, создаваемые mocking-фреймворком (Mockito в нашем случае). Как я уже говорил выше, они очень полезны для тестирования изолированного кода. Главная задача при этом — убедиться, что ваш мок ведет себя аналогично реальному коду. В противном случае тесты станут бессмысленными. Всеволод Брекелов: Да, есть мнение: один юнит тест — один assertion. На практике такое я видел очень редко. Думаю, что это уже философия команды. Множественные assertions вполне себе имеют место. Если мы проводим юнит тесты, а не компонентные, то все зависимости изолируем (моки, стабы — все в ваших руках). Тут нет каких-то сложностей на мой взгляд. А если и появляются, то StackOverflow точно поможет. Так как я пишу на Java/JavaScript(Angular), то использую обычные популярные тулы: на Java – Mockito/EasyMock. Для компонентных тестов написать свой responsive mock — тоже хорошая идея! Всем советую. JavaScript – ngMock. Кстати, для компонентых тестов очень классная тема – AngularPlayground. — Как найти компромисс между трудовыми и финансовыми затратами на тестирование и качеством итогового софта при реализации «горящих» проектов? Как обычно вы аргументируете важность полноценного тестирования в таких случаях? Marc Philipp: По моему опыту, вы не можете спасти «горящий» проект, пропустив тесты. Написание unit-тестов является неотъемлемой частью разработки программного обеспечения. Без него у вас нет возможности узнать, действительно ли ваш код выполняет то, что, по вашему мнению, он должен делать. Вы не сможете ничего быстро починить, так как не поймете, где что сломалось. Как сказалUncleBob, «единственный способ быстро поехать — это хорошо идти». Всеволод Брекелов: Думаю, тут нет однозначного ответа. Скорее, помогает опыт работы и тип проекта. Если вы делаете медицинский проект или строите ракету, то о важности тестирования не приходиться говорить. Если пилите стартап за неделю – то какие тесты? Очень важно организовать процесс, чтобы избежать внезапных багов и неправильно реализованных требований. Что такое правильный процесс? Конечно, есть Agile Manifesto, на который многие смотрят при организации процесса, но все равно что-то не выходит. Можно взять и построить процесс ради процесса. А можно и наоборот, последовать за http://programming-motherfucker.com/. Мне кажется, главное – иметь требования, детализация которых устраивает разработчиков и тестировщиков в команде. Это значит, что у них одинаковое понимание того, что будет на выходе. — Какие приемы помогают сократить время и трудовые затраты на тестирование? Marc Philipp: «Тестирование» — перегруженный термин. Это может означать что угодно: модульное тестирование, ручное тестирование, тестирование производительности… По моему опыту, ручное тестирование, то есть ручное выполнение плана пошагового прохождения тестовых примеров, действительно дорого и часто не так эффективно, как вы думаете. Более того, автоматизация этих скучных тестов имеет смысл только в определенной степени. Тем не менее, вы должны действительно следовать тестовой пирамиде, а не писать слишком много этих end-to-end/UI тестов. Большинство ваших тестов должны быть реальными unit-тестами: независимые, быстрые тесты, которые вы можете выполнять очень часто. Написание этих тестов относительно дешево, особенно если вы знаете свои инструменты. Они очень надежны, поэтому вы не будете тратить время на их актуализацию. UI и Integration тесты всегда будут более хрупкими из-за огромного количества задействованных компонентов. Всеволод Брекелов: Есть хороший прием — писать меньше кода. Главное – это понимание процесса и того, что вы хотите решить (или протестировать). Всегда нужно адекватно оценивать бюджет и время. Что это значит? Если вы можете себе позволить вливать кучу денег в приближение к 100% coverage — why not? Хозяин – барин. Если у вас нет денег на автотесты (которые, как известно, отбиваются в основном в долгоиграющих проектах), то толпа ручных тестировщиков – ваш вариант. Если не впадать в крайности, то самая частая ошибка — это написание e2e тестов пачками до потери пульса до того, как написаны юнит тесты, компонентные тесты, интеграционные тесты на Backend, Frontend, DB, Performance и тд. Эта тенденция, вероятно, следует от модных BDD подходов (я их не очень люблю). К чему это все приводит? Первая степень «опьянения» — у вас начинает реально работать автоматизация. Ручные тест кейсы вы заменяете на автоматические. Тестировщики начинают радоваться. Менеджеры начинают думать, что вот-вот сэкономят. Вторая степень — тестов становится много, почему-то некоторые из них периодически падают. Тестировщики уже не очень рады. Нужно сидеть и разбираться в причинах. А баги все равно пролезают. И, вероятно, даже находятся на QA окружении путем ручного (может, даже monkey) тестирования. Третья степень — все начинают ходить на конференции про Selenium (ничего не имею против этих конференций), узнавать как бороться с Flaky тестами, пробовать различные решения. Пускать тесты в параллель. Четвертая степень — строить целые суперархитектуры по запуску 500 e2e тестов на 50 агентах, чтобы все летало быстро, аж за 10 минут (я тут утрирую, конечно). И все равно баги есть. Пятая степень — я назову ее недостижимой. Приходит осознание того, что бОльшая часть e2e тестов не нужна. Нужны другие тесты, которых никто никогда не писал. Например, компонентные тесты на back-end или они же на UI. А может, не они, может, системные тесты? А может, и тесты на верстку? А может, Ваш, {username} вариант? Безусловно есть проекты, где все сделано «правильно». Но зачастую встречается проблема непонимания того, что нужно протестировать. Только правильное понимание может сохранить ваше время и финансы. И более того, улучшить качество продукта. — Как влияет на инструменты и подходы тестировщиков развитие средств разработки и подходов к созданию кода? Что из новшеств облегчает unit-тестирование (например, представление методов в виде лямбда-функций)? Marc Philipp: Новые инструменты стараются облегчить жизнь разработчикам, предоставляя им большую гибкость. Однако, в конце концов, я считаю, что не имеет значения, представляете ли вы свои тесты как методы или в виде лямбда-функций. Понять, что тестировать и как тестировать, — это самая сложная часть. Всеволод Брекелов: Развитие средств и подходов влияет позитивно, если ими пользуются. Не всегда есть возможность применить хайповые технологии или подходы на работе. Мы все-таки решаем бизнес-задачи. Но находить баланс всегда можно. Что облегчает тестирование — странный вопрос. Думаю, что технологии не могут сильно облегчить жизнь. Так как, чтобы использовать что-то новое (технология, инструмент), его нужно изучить всей команде, принять какую-ту «полиси», code style. Это в перспективе может, конечно, облегчить жизнь, но на коротких дистанциях не очень полезно, так как трудозатратно, имхо. Кстати, вариант перехода на Kotlin (если мы говорим про Java тесты) – может и неплохая идея. Я в своей практике пока не пробовал. Касательно новшеств языка (лямбды и прочие полезности) — это все хорошо, конечно, но мне трудно сказать, насколько они облегчают жизнь, так как нужно это измерить. Я не измерял. Но только не записывайте меня в противники прогресса, я считаю, что практика по изучению/использованию чего-то нового должна присутствовать всегда. Это обычная continuos improvement история. — Насколько вы покрываете unit-тестами ваши продакшн проекты? Стоит ли тратить время на 100% покрытие? Marc Philipp: В зависимости от языка программирования и фреймворков, которые вы используете, в проекте может быть некоторый шаблонный код, который не содержит никакой логики. Но кроме таких кусков, на мой взгляд, вы должны написать unit-тесты для всего вашего кода. Таким образом, я бы посоветовал охват более 90%. Всеволод Брекелов: В проектах, в которых мне приходилось работать, чаще всего разработчики стараются довести тесты до покрытия в 90%. Стоит ли тратить время – обычно решается менеджерами. Я не менеджер, но по мне юнит тесты – это очень хорошая практика, 100% покрытие хорошо иметь, когда есть на это ресурсы. Главное, надо помнить, что 100% покрытие, к сожалению, не гарантирует, что у вас нет багов. Из того, что кажется более полезным, чем гонка с 90% до 100% coverage, — это написание мутационных тестов. Ничего не скажу нового относительно статьи 2012 года. Но на практике не очень часто видел, чтобы применяли этот подход (да и сам я тоже, каюсь). Так может быть пора начинать? — Как тестовые фреймворки помогают с unit-тестами? Какую часть работ они берут на себя? Чего не стоит ждать при использовании фреймфорков? Marc Philipp: Хороший фреймворк позволяет очень быстро и легко писать простые unit-тесты и в то же время содержать мощные механизмы для проведения более сложных тестов. Например, он должен помочь вам подготовить тестовые данные и предоставить точки расширения, которые позволят вам повторно использовать одну и ту же логику во многих тестах. Но никакой фреймворк не решит за вас, что и как тестировать. Также он не может волшебным образом улучшить ваш проект, чтобы сделать его хорошо тестируемым. — Какие элементы кода сложнее всего поддаются unit-тестированию? Как решается эта проблема у вас? Всеволод Брекелов: Чем больше зависимостей — тем больше рутины, тем сложнее писать юнит тест. А в целом, не вижу каких-то особенных проблем, если честно. Хотя на тему unit тестов написано большое количество книг, из которых я ни одну не прочитал до конца. Может, поэтому я не обременен проблемами. Например, сложно написать unit-тест, когда, скажем, конструктор объекта содержит в себе вермишели кода, но тогда можно советовать товарищам прочитать книжки, например и ввести code review практику. Что касается JavaScript кода, то там можно встретиться с различными сложностями и внезапностями (да, я очень люблю JavaScript), скорее связанными с используемым фреймворком, например, работа с digest’ом. Я использовал только AngularJS/Angular2/Angular4. Несмотря на старания команды Angular сделать удобно-тестируемый фреймворк, все равно периодически сталкиваешься с проблемами, которые безусловно имеют решения, мы ведь инженеры. Обсудить в форуме. |