Введение в тестирование
Опубликовано: 2021-06-16Добро пожаловать в нашу новую серию блогов обо всем, что связано с тестированием . Надеемся, что эти сообщения в блоге дадут вам общее представление о том, как мы пишем их здесь, в Mediatoolkit, и почему. Прежде чем мы углубимся в примеры, давайте начнем с некоторых основных определений, идей и преимуществ написания тестов.
Какие тесты существуют?
Не все тесты одинаковы. Существуют разные виды тестов для разных целей. Хотя этот пост посвящен в основном модульным тестам , мы должны знать о различиях и понимать преимущества каждого из них.
1. Ручные тесты
Ручное тестирование включает тесты, выполняемые тестировщиком вручную. Обычно это делается службой обеспечения качества или самими разработчиками . QA следует рассматривать как последнюю линию защиты, а не как основного тестировщика приложений (QA, выполняющее ручное тестирование как основной способ тестирования пользовательского интерфейса, является допустимым исключением в этом случае).
Разработчики обычно пишут ручные тесты, когда хотят запустить что-то локально и посмотреть, как ведет себя система, не отправляя какой-либо код на стадию подготовки или, упаси вас Бог, в производство. Однако целью этих тестов является не надежность кода. Единственная роль этих тестов — отлавливать ошибки и обеспечивать качество вашего продукта .
2. Интеграционные тесты
Системы состоят из нескольких компонентов, или, по крайней мере, так и должно быть. Интеграционные тесты проверяют общедоступный API этих компонентов. Не только REST API, но и любой общедоступный API, например, общение в теме Kafka.
Если компонент «говорит», он выводит сообщение по теме в определенном формате, то есть его публичный API или контракт. Интеграционное тестирование проверяет, могут ли несколько отдельно разработанных компонентов обмениваться данными и совпадают ли их контракты в группе.
Если вы протестируете эти компоненты только по отдельности, их индивидуальное поведение может работать правильно, но когда вы попытаетесь соединить их в большую группу, вы обнаружите, что их контракты различаются .
3. Сквозные (E2E) тесты
Сквозное тестирование обеспечивает удобство работы пользователей с конечным продуктом . Сложность современных систем трудно покрыть тестами. Многие системы полагаются друг на друга, и E2E-тесты гарантируют, что продукт работает так, как ожидается . QA проверяет правильность вашего продукта, просматривая поток конечных пользователей и проверяя, все ли системы ведут себя должным образом.
4. Модульные тесты
Модульные тесты являются основой любого надежного программного обеспечения . Они составляют основу для других тестов. Они тестируют отдельные блоки.
Разработчики могут ошибиться в определении единицы с одним методом. Модульные тесты должны проверять поведение компонента, который может состоять из нескольких разных классов. Публичные методы, доступные другим компонентам, необходимо тестировать, не тестируйте защищенные методы или классы. Это детали реализации, а не часть вашего общедоступного API .
Зачем нам вообще тестировать?
Если мы будем писать хорошие тесты и писать их часто, мы сможем гарантировать качество нашего продукта еще до того, как он увидит свет.
По мере старения нашей системы преимущества тестирования становятся все более и более очевидными. Наши среды становятся надежными, они экономят время на разработку и избавляют от лишних хлопот, когда неизбежно что-то пойдет не так. Ваши коллеги будут благодарны, если смогут взглянуть на ваши тесты и понять, что должен и чего не должен делать ваш код, не запуская его вручную .
Надежный код
Прежде чем мы начнем говорить о «надежном коде», как его определить? Что делает код надежным? Означает ли это, что мы должны программировать с осторожностью и думать о том, как другие разработчики могут злоупотреблять нашим кодом?
Надежный код всегда прост и чист. Простота надежна, сложность хрупка . Мы должны обрабатывать недопустимые входные данные, но это не значит, что нам нужно защищаться и не доверять нашей команде.
Закон Галла : Сложная система, которая работает, всегда оказывается развившейся из простой системы, которая работала.
Справедливо и обратное утверждение: сложная система, разработанная с нуля, никогда не работает и ее нельзя заставить работать. Вы должны начать все сначала, начиная с работающей простой системы.
Безопасный рефакторинг
Когда ваш код покрыт тестами, вы больше не боитесь изменить существующий код . После каждого изменения вы можете запустить свои тесты и убедиться, что ничего не сломалось. Когда у вас есть тесты, вам не нужно защищаться от программ.
Рефакторинг без тестов идет по скользкой дорожке, которая закончится бессонными ночами и рабочими воскресеньями. Эта тема слишком широка, чтобы описывать ее здесь, и заслуживает отдельного поста в блоге в будущем.
Как мы не пишем тесты?
Знать, как НЕ писать модульные тесты , так же важно, как и как их писать.
- Написание теста, выводящего результат вызова метода, не является тестом, поскольку мы не проверяем желаемый результат .
- Если ваш тест считывает данные из файла в папке «Документы», это не настоящий модульный тест, поскольку тесты не должны зависеть от среды.
- Любой разработчик должен иметь возможность проверить ваш код и успешно запустить тесты, ничего не делая.
- Каждый модульный тест должен быть независимым от других тестов . Это означает, что порядок выполнения ваших тестов также не должен иметь значения.
- Многократный запуск тестов всегда должен заканчиваться одними и теми же результатами , если мы не меняем код.
- Модульные тесты должны проверять поведение , а не вызовы отдельных методов. Не каждый класс и метод должен иметь свой тест.
Поведение — это то, что создает реальную ценность, которая нужна пользователю вашей системы. Нужно ли вашему пользователю знать, создал ли productFactory.create() один и тот же объект при двойном вызове или ваш репозиторий был вызван с некоторыми параметрами? Наверное, нет, но все же многие разработчики пишут именно такие тесты.
Если ваши тесты выглядят так, они тесно связаны с вашей реализацией. Каждый раз, когда вы хотите изменить детали своей реализации, вам нужно обновлять свои тесты, даже если поведение остается прежним. Ваши тесты должны меняться только при изменении поведения, а не деталей реализации . Другими словами, проверяйте, что делает ваш код, а не как он это делает.
Как мы пишем тесты?
Наши тесты должны следовать лучшим практикам написания кода , они должны быть независимыми от среды и выполняться быстро .
Важно, чтобы время выполнения теста было как можно короче. Каждый тест не должен занимать более пары миллисекунд . Когда выполнение тестов занимает слишком много времени, люди, как правило, пропускают их и просто полагаются на свой CI-сервер, такой как Jenkins, который поднимает шум, когда он не может создать исполняемые файлы для развертывания.
Каждый тест состоит из 3 разделов «А» (шаблон ААА) :
- Договариваться
- действовать
- Утверждать
1. Упорядочить
В разделе аранжировки нашего теста мы убеждаемся, что наша система находится в определенном состоянии, прежде чем вызывать поведение, которое мы хотим протестировать . «Система» может быть объектом, который нам нужно настроить определенным образом для создания поведения, создания временных файлов или чего-то подобного.

Код в этом разделе обычно больше, чем в двух других вместе взятых .
Одним из шаблонов проектирования, который должен оказаться особенно полезным для уменьшения размера этого раздела, является Object Mother . Этот шаблон проектирования очень похож на Factory , но имеет более конкретные методы, которые создают для вас предварительно настроенные объекты. В то время как стандартная Factory может иметь такой метод, как createCar(carDescription) , ObjectMother будет иметь такие методы, как createRedFerrari() , createBlackTesla() или createBrokenYugo() .
2. Закон
Этот раздел вашего теста должен состоять из одной строки . Эта строка выполняет тестируемое поведение. Если вы обнаружите, что пишете больше одной строки для этого раздела, вероятно, у вас нет правильной инкапсуляции вашего поведения . Не следует ожидать, что ваши клиенты будут вызывать несколько методов вашего объекта в определенном порядке, так зачем ваши тесты?
Эта строка является вызовом метода, который мы хотим протестировать. Если этот метод возвращает результат, вы должны сохранить это значение в переменной, чтобы проверить, является ли оно ожидаемым значением на этапе Assert.
3. Утверждать
После того, как мы подготовили систему в разделе Arrange и выполнили тестируемое действие в разделе Act, нам нужно проверить результат действия. Обычно мы проверяем результат метода здесь, но иногда наши методы не возвращают значения, но все же производят побочные эффекты. Если наш код должен был изменить состояние объекта, создать файл или удалить что-то из List , мы должны проверить, сделал ли он именно это.
Заглушки против моков
Большинство разработчиков используют термины mock и stub как синонимы, но между ними есть различия.
Заглушка не может провалить тест, макет может.
Заглушки основаны на состоянии , они возвращают жестко закодированные значения. «Какой результат я получил?»
Моки основаны на поведении , вы используете их, чтобы проверить, как ваше поведение проходит через них. «Как я получил результат?»
Если ваши модульные тесты завалены моками, ваши тесты в конечном итоге становятся очень хрупкими, а это означает, что каждый раз, когда вы меняете одну из деталей реализации, вам нужно обновлять все ваши фиктивные вызовы.
Тестируйте только одно.
Нам нужно иметь возможность изолировать одно поведение и доказать, что оно работает . Если это поведение должно работать по-разному с разными входными данными, нам нужно написать новый тест для каждого из этих поведений. Трудно понять, почему наш тест не удался, если у нас есть большой тест, который тестирует несколько вещей одновременно. Более того, становится все труднее удалять функции, которые нам больше не нужны, и смотреть, какие функции мы сломали при добавлении нового кода.
Совершенно нормально иметь несколько утверждений в последнем разделе ваших тестов, если это не требует, чтобы вы вызывали тестируемое поведение несколько раз. Если это так, будет трудно точно определить неисправное поведение и исправить его.
Когда мы заявляем о нескольких вариантах поведения в одном тесте, мы не получаем четкой картины того, что именно не работает, потому что тест сообщит только о первом сбое, а остальные будут пропущены. Становится намного сложнее понять, какие изменения необходимы и сколько вещей работает не так, как ожидалось.
Именование тестов
Одна вещь, которая отличает хорошие тесты от хороших, — это название теста. Тесты должны сообщать нам не только о том, что они делают, но и о том, когда они это делают.
Есть много хороших шаблонов именования, которые вы можете использовать, поэтому выберите тот, который вы считаете наиболее описательным, и придерживайтесь его. Вот несколько примеров удачных названий тестов:
- RegistrationServiceShould.createNewAccountWhenEmailIsNotTaken
- RegistrationServiceTest.whenEmailIsFree_createNewAccount
- RegistrationServiceTest.if_freeEmail_when_userCreatesAccount_then_create
Когда вы пишете модульные тесты, гораздо важнее сообщить, что вы тестируете, чем следовать лучшим практикам именования методов. Например, в Java мы используем camelCase при написании методов, но вполне допустимо использовать подчеркивание (_), чтобы отделить состояние от действия в имени вашего теста.
Чистые тесты
Тесты, которые вы пишете, должны следовать всем практикам чистого кода, которые вы применяете к своему коду. Тесты не являются гражданами второго сорта, и вам нужно применять тот же уровень осторожности, что и к остальной части вашего кода, чтобы сделать их читабельными.
Определение дублирования кода в тестах очень важно. Принцип DRY («Не повторяйся») применяется к извлечению поведения, которое изменяется по одной и той же причине. Тесты меняются по разным причинам, поэтому извлекайте из тестов разные вещи, если они действительно не меняются по одной и той же причине. Спойлер, их часто нет.
if операторы не относятся к тестам. Оператор if говорит нам, что наш тест выполняет как минимум две разные вещи, и было бы лучше, если бы мы переписали наш тест как два разных теста. При правильном названии будет легче понять, что делают тесты и каково их поведение.
Когда мы должны писать тесты?
Руководствуясь принципами TDD, мы должны писать тесты перед написанием нового кода .
Когда нам нужно добавить новую функцию, мы сначала описываем желаемое поведение как новый тест. Мы вносим наименьшее количество изменений, необходимых для прохождения этого теста без нарушения других.
В противном случае, чем больше времени проходит, тем больше у нас остается кода, который не был протестирован, и вероятность внесения ошибок или чрезмерной инженерии возрастает .
Кроме того, тесты становятся более сложными, так как теперь у нас есть больше кода для тестирования, и что обычно происходит, так это то, что разработчики подстраивают тесты под код. Это означает, что мы корректируем поведение в соответствии с тем, что делает наш код, а не наоборот.
Когда мы пишем тесты на ранней стадии, определение проблемы становится меньше, и нам легче разобраться с такими проблемами, чем с более общим и сложным поведением.
Последние мысли
Хотя написание тестов может показаться необязательным занятием, очень важно начать с хорошей основы. Программирование уже сложно, сделайте его проще для себя и своих товарищей по команде, написав тестируемый код, который легче читать, понимать и поддерживать . Наконец, если у вас есть трудности с тем, чтобы ваши тесты соответствовали вашим пожеланиям, проблема, скорее всего, в вашем коде, а не в ваших тестах.
Заинтересованы в работе над чистым и тестируемым кодом?
Ознакомьтесь с нашей открытой вакансией Senior Backend Developer
или отправьте нам открытую заявку !
