Как наконец-то начать писать тесты и не пожалеть об этом
Приходя на новый проект, я регулярно сталкиваюсь с одной из следующих ситуаций:
- Тестов нет совсем.
- Тестов мало, их редко пишут и не запускают на постоянной основе.
- Тесты присутствуют и включены в CI (Continuous Integration), но приносят больше вреда, чем пользы.
Что можно сделать, чтобы изменить сложившуюся ситуацию? Идея использования тестов не нова. При этом большинство туториалов напоминают знаменитую картинку про то, как нарисовать сову: подключаем JUnit, пишем первый тест, используем первый мок — и вперед! Такие статьи не отвечают на вопросы о том, какие тесты нужно писать, на что стоит обращать внимание и как со всем этим жить. Отсюда и родилась идея данной статьи. Я постарался кратко обобщить свой опыт внедрения тестов в разных проектах, чтобы облегчить этот путь для всех желающих.

Совсем вводных статей по данной теме более чем достаточно, поэтому не будем повторяться и попытаемся зайти с другой стороны. В первой части развенчаем миф о том, что тестирование несет исключительно дополнительные затраты. Будет показано, как создание качественных тестов может в свою очередь ускорить процесс разработки. Затем на примере небольшого проекта будут рассмотрены базовые принципы и правила, которых стоит придерживаться, чтобы эту выгоду реализовать. Наконец, в заключительном разделе будут даны конкретные рекомендации по внедрению: как избежать типичных проблем, когда тесты начинают, наоборот, существенно тормозить разработку.
Так как моя основная специализация — Java backend, то в примерах будет использован следующий стек технологий: Java, JUnit, H2, Mockito, Spring, Hibernate. При этом значительная часть статьи посвящена общим вопросам тестирования и советы в ней применимы к гораздо более широкому кругу задач.
Однако будьте осторожны! Тесты вызывают сильнейшую зависимость: однажды научившись ими пользоваться, вы уже не сможете без них жить.
Содержание
Тесты vs скорость разработки
Главные вопросы, которые возникают при обсуждении внедрения тестирования: сколько времени займет написание тестов и какие преимущества это будет иметь? Тестирование, как и любая другая технология, потребует серьезных усилий на освоение и внедрение, поэтому на первых порах никакой значимой выгоды ожидать не стоит. Что касается временных затрат, то они сильно зависят от конкретной команды. Однако меньше чем на 20–30 % дополнительных затрат на кодирование рассчитывать точно не стоит. Меньшего просто не хватит для достижения хоть какого-то результата. Ожидание мгновенной отдачи часто является главной причиной сворачивания этой деятельности еще до того, как тесты станут приносить пользу.
Но о какой же тогда эффективности идет речь? Давайте отбросим лирику о трудностях внедрения и посмотрим, какие конкретные возможности по экономии времени открывает тестирование.
Запуск кода в произвольном месте
При отсутствии тестов в проекте единственным способом запуска является поднятие приложения целиком. Хорошо, если на это будет уходить секунд 15–20, но далеко не редки случаи больших проектов, в которых полноценный запуск может занимать от нескольких минут. Что же это означает для разработчиков? Существенную часть их рабочего времени будут составлять эти короткие сессии ожидания, на протяжении которых нельзя продолжать работать над текущей задачей, но при этом времени на переключение на что-то другое слишком мало. Многие хотя бы раз сталкивались с такими проектами, где написанный за час код требует многочасовой отладки из-за долгих перезапусков между исправлениями. В тестах же можно ограничиться запуском маленьких частей приложения, что позволит значительно сократить время ожидания и повысит продуктивность работы над кодом.
Кроме того, возможность запуска кода в произвольном месте ведет к более тщательной отладке. Зачастую проверка даже основных позитивных сценариев использования через интерфейс приложения требует серьезных усилий и времени. Наличие же тестов позволяет проводить детальную проверку конкретного функционала гораздо проще и быстрее.
Еще один плюс — возможность регулирования размера тестируемого юнита. В зависимости от сложности проверяемой логики, можно ограничиться одним методом, классом, группой классов, реализующих некоторую функциональность, сервисом и так далее, вплоть до автоматизации тестирования приложения целиком. Такая гибкость позволяет разгрузить высокоуровневые тесты от многих деталей за счет того, что они будут проверены на более низких уровнях.
Повторный запуск тестов
Этот плюс часто приводят как суть автоматизации тестирования, однако давайте рассмотрим его под менее привычным углом зрения. Какие новые возможности для разработчиков он открывает?
Во-первых, каждый новый пришедший на проект разработчик сможет легко запустить имеющиеся тесты, чтобы разобраться в логике приложения на примерах. К сожалению, важность этого сильно недооценена. В современных условиях одни и те же люди редко работают над проектом дольше 1–2 лет. А так как команды состоят из нескольких человек, то появление нового участника каждые 2–3 месяца — типичная ситуация для относительно крупных проектов. Особо тяжелые проекты переживают смены целых поколений разработчиков! Возможность легко запустить любую часть приложения и посмотреть на поведение системы в разы упрощает погружение новых программистов в проект. Кроме того, более детальное изучение логики кода уменьшает количество допущенных ошибок на выходе и время на их отладку в будущем.
Во-вторых, возможность легко убедиться в том, что приложение работает корректно, открывает дорогу для непрерывного рефакторинга (Continuous Refactoring). Этот термин, к сожалению, гораздо менее популярен, чем CI. Он означает, что рефакторинг можно и нужно делать при каждой доработке кода. Именно регулярное следование небезызвестному правилу бойскаута «оставь место стоянки чище, чем оно было до твоего прихода», позволяет избегать деградации кодовой базы и гарантирует проекту долгую и счастливую жизнь.
Отладка
Отладка уже была упомянута в предыдущих пунктах, но этот момент настолько важен, что заслуживает более внимательного рассмотрения. К сожалению, не существует достоверного способа измерить соотношение между временем, потраченным на написание кода и на его отладку, так как эти процессы практически неотделимы друг от друга. Тем не менее наличие качественных тестов в проекте существенно сокращает время отладки, вплоть до почти полного отсутствия необходимости запускать дебаггер.
Эффективность
Все перечисленное может дать существенную экономию времени на первичную отладку кода. При правильном подходе только это уже окупит все дополнительные затраты на разработку. Остальные бонусы тестирования — повышение качества кодовой базы (плохо спроектированный код тяжело тестировать), уменьшение количества дефектов, возможность убедиться в корректности кода в любой момент и т. д. — достанутся практически бесплатно.
От теории к практике
На словах это все выглядит неплохо, но давайте перейдем к делу. Как уже было сказано ранее, материалов о том, как произвести первичную настройку тестовой среды, более чем достаточно. Потому сразу перейдем к готовому проекту. Исходники тут.
Задача
В качестве шаблонной задачки рассмотрим небольшой фрагмент бэкенда интернет-магазина. Напишем типовой API для работы с продуктами: создание, получение, редактирование. А также пару методов для работы с клиентами: смена «любимого продукта» и расчет бонусных баллов по заказу.
Доменная модель
Чтобы не перегружать пример, ограничимся минимальным набором полей и классов.

У клиента (Customer) есть логин, ссылка на любимый продукт и флаг, указывающий на то, является ли он премиальным клиентом.
У продукта (Product) — название, цена, скидка и флаг, указывающий на то, рекламируется ли он в данный момент.
Структура проекта
Структура основного кода проекта выглядит следующим образом.

Классы разбиты по слоям:
- Model — доменная модель проекта;
- Jpa — репозитории для работы с БД на основе Spring Data;
- Service — бизнес-логика приложения;
- Controller — контроллеры, реализующие API.

Классы тестов лежат в тех же пакетах, что и оригинальный код. Дополнительно создан пакет с билдерами для подготовки тестовых данных, но об этом ниже.
Удобно разделять юнит-тесты и интеграционные тесты. Они зачастую имеют разные зависимости, и для комфортной разработки должна быть возможность запустить либо одни, либо другие. Этого можно добиться разными способами: конвенции именования, модули, пакеты, sourceSets. Выбор конкретного способа — исключительно вопрос вкуса. В данном проекте интеграционные тесты лежат в отдельном sourceSet — integrationTest.

Подобно юнит-тестам, классы с интеграционными тестами лежат в тех же пакетах, что и оригинальный код. Дополнительно есть базовые классы, которые помогают избавиться от дублирования конфигурации и при необходимости содержат полезные универсальные методы.
Интеграционные тесты
Есть разные подходы к тому, с каких тестов стоит начинать. В случае, если проверяемая логика не очень сложна, можно сразу переходить к интеграционным (их еще иногда называют приемочными — acceptance). В отличие от юнит-тестов они позволяют убедиться, что приложение в целом работает корректно.
Архитектура
Для начала надо определиться, на каком конкретно уровне будут выполняться интеграционные проверки. Spring Boot предоставляет полную свободу выбора: можно поднимать часть контекста, весь контекст и даже полноценный сервер, доступный из тестов. При увеличении размера приложения этот вопрос становится все более сложным. Часто приходится писать разные тесты на разных уровнях.
Хорошей точкой старта будут тесты контроллеров без запуска сервера. В относительно небольших приложениях вполне приемлемо поднимать весь контекст целиком, так как по умолчанию он переиспользуется между тестами и инициализируется только один раз. Рассмотрим основные методы класса ProductController :
@PostMapping("new") public Product createProduct(@RequestBody Product product) < return productService.createProduct(product); >@GetMapping("") public Product getProduct(@PathVariable("productId") long productId) < return productService.getProduct(productId); >@PostMapping("/edit") public void updateProduct(@PathVariable("productId") long productId, @RequestBody Product product)
Вопрос обработки ошибок оставим в стороне. Предположим, что она реализована снаружи на основе анализа выбрасываемых исключений. Код методов очень простой, их реализация в сервисе ProductService не сильно сложнее:
@Transactional(readOnly = true) public Product getProduct(Long productId) < return productRepository.findById(productId) .orElseThrow(() ->new DataNotFoundException("Product", productId)); > @Transactional public Product createProduct(Product product) < return productRepository.save(new Product(product)); >@Transactional public Product updateProduct(Long productId, Product product) < Product dbProduct = productRepository.findById(productId) .orElseThrow(() ->new DataNotFoundException("Product", productId)); dbProduct.setPrice(product.getPrice()); dbProduct.setDiscount(product.getDiscount()); dbProduct.setName(product.getName()); dbProduct.setIsAdvertised(product.isAdvertised()); return productRepository.save(dbProduct); >
Репозиторий ProductRepository вообще не содержит собственных методов:
public interface ProductRepository extends JpaRepository
Все намекает на то, что юнит-тесты этим классам не нужны просто потому, что всю цепочку можно легко и эффективно проверить несколькими интеграционными тестами. Дублирование одних и тех же проверок в разных тестах приводит к усложнению отладки. В случае появления ошибки в коде теперь упадет не один тест, а сразу 10–15. Это в свою очередь потребует дальнейшего анализа. Если же дублирования нет, то единственный упавший тест, скорее всего, сразу укажет на ошибку.
Конфигурация
Для удобства выделим базовый класс BaseControllerIT , который содержит конфигурацию Spring и пару полей:
@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) @Transactional public abstract class BaseControllerIT
Репозитории вынесены в базовый класс, чтобы не захламлять классы тестов. Их роль исключительно вспомогательная: подготовка данных и проверка состояния базы после работы контроллера. При увеличении размера приложения это может перестать быть удобным, но для начала вполне подойдет.
Основная конфигурация Spring задается следующими строчками:
@SpringBootTest — используется для того, чтобы задать контекст приложения. WebEnvironment.NONE означает, что веб-контекст поднимать не надо.
@Transactional — оборачивает все тесты класса в транзакцию с автоматическим откатом для сохранения состояния базы.
Структура теста
Перейдем к минималистичному набору тестов для класса ProductController — ProductControllerIT .
@Test public void createProduct_productSaved()
Код теста должен быть предельно прост и понятен с первого взгляда. Если это не так, то большая часть плюсов тестов, описанных в первом разделе статьи, теряется. Хорошей практикой является разделение тела теста на три визуально отделяемые друг от друга части: подготовка данных, вызов тестируемого метода, валидация результатов. При этом очень желательно, чтобы код теста помещался на экране целиком.
Лично мне кажется более наглядным, когда тестовые значения из секции подготовки данных используются потом и в проверках. Альтернативно можно было бы явно сравнивать объекты, например так:
assertEquals(product, dbProduct);
В другом тесте на обновление информации о продукте ( updateProduct ) видно, что создание данных стало немного сложнее и для сохранения визуальной целостности трех частей теста они отделены двумя переводами строк подряд:
@Test public void updateProduct_productUpdated()
Каждую из трех частей теста можно упростить. Для подготовки данных отлично подходят тестовые билдеры, которые содержат в себе логику создания объектов, удобную для использования из тестов. Слишком сложные вызовы методов можно выносить во вспомогательные методы внутри тестовых классов, скрывая часть нерелевантных для данного класса параметров. Для упрощения сложных проверок можно также писать вспомогательные функции либо реализовывать собственные матчеры. Главное при всех этих упрощениях — не потерять наглядности теста: все должно быть понятно с первого взгляда на основной метод, без необходимости перехода вглубь.
Тестовые билдеры
Тестовые билдеры заслуживают отдельного внимания. Инкапсуляция логики создания объектов упрощает сопровождение тестов. В частности, заполнение не релевантных данному тесту полей модели можно скрыть внутри билдера. Для этого нужно не создавать его напрямую, а использовать статический метод, который заполнит недостающие поля значениями по умолчанию. Например, в случае появления новых обязательных полей в модели их можно будет легко добавить в этот метод. В ProductBuilder он выглядит так:
public static ProductBuilder product(String name)
Название теста
Крайне важно понимать, что конкретно проверяется в данном тесте. Для наглядности лучше всего дать ответ на этот вопрос в его названии. На примере тестов для метода getProduct рассмотрим используемую конвенцию именования:
@Test public void getProduct_oneProductInDb_productReturned() < Product product = product("productName").build(); productRepository.save(product); Product result = productController.getProduct(product.getId()); assertEquals("productName", result.getName()); >@Test public void getProduct_twoProductsInDb_correctProductReturned()
В общем случае заголовок тестового метода состоит из трех частей, разделенных подчеркиванием: имя тестируемого метода, сценарий, ожидаемый результат. Однако здравый смысл никто не отменял, и вполне оправданным может быть опускание каких-то частей названия, если они не нужны в данном контексте (например, сценарий в единственном тесте на создание продукта). Цель такого именования — добиться того, чтобы суть каждого теста была понятна без изучения кода. Это делает окошко результатов прохождения тестов максимально наглядным, а именно с него обычно и начинается работа с тестами.

Вот и все. На первое время минималистичного набора из четырех тестов вполне достаточно для проверки методов класса ProductController . В случае выявления багов всегда можно будет добавить недостающие тесты. При этом минимальное количество тестов значительно сокращает время и силы на их поддержку. В свою очередь это является критичным в процессе внедрения тестирования, так как первые тесты обычно получаются не самого лучшего качества и создают много неожиданных проблем. В то же время такого тестового набора вполне достаточно для получения бонусов, описанных в первой части статьи.
Стоит обратить внимание, что такие тесты не проверяют веб-слой приложения, однако зачастую этого и не требуется. При необходимости можно написать отдельные тесты для веб-слоя с заглушкой вместо базы ( @WebMvcTest , MockMvc , @MockBean ) или использовать полноценный сервер. Последнее может затруднить отладку и усложнить работу с транзакциями, поскольку транзакцию сервера тест уже контролировать не сможет. Пример такого интеграционного теста можно посмотреть в классе CustomerControllerServerIT .
Юнит-тесты
Юнит-тесты имеют ряд преимуществ перед интеграционными:
- Запуск занимает миллисекунды;
- Небольшой размер тестируемого юнита;
- Легко реализовать проверку большого количества вариантов, так как при вызове метода напрямую подготовка данных значительно упрощается.
Единственный класс в данном примере, который заслуживает юнит-тестирования, — это BonusPointCalculator . Его отличительная особенность — большое количество ветвлений бизнес-логики. Например, предполагается, что покупатель получает бонусами 10 % от стоимости продукта, помноженные на не более чем 2 мультипликатора из следующего списка:
- Продукт стоит больше 10 000 (× 4);
- Продукт участвует в рекламной кампании (× 3);
- Продукт является «любимым» продуктом клиента (× 5);
- Клиент имеет премиальный статус (× 2);
- В случае, если клиент имеет премиальный статус и покупает «любимый» продукт, вместо двух обозначенных мультипликаторов используется один (× 8).
private List calculateMultipliers(Customer customer, Product product) < Listmultipliers = new ArrayList<>(); if (customer.getFavProduct() != null && customer.getFavProduct().equals(product)) < if (customer.isPremium()) < multipliers.add(PREMIUM_FAVORITE_MULTIPLIER); >else < multipliers.add(FAVORITE_MULTIPLIER); >> else if (customer.isPremium()) < multipliers.add(PREMIUM_MULTIPLIER); >if (product.isAdvertised()) < multipliers.add(ADVERTISED_MULTIPLIER); >if (product.getPrice().compareTo(EXPENSIVE_THRESHOLD) >= 0) < multipliers.add(EXPENSIVE_MULTIPLIER); >return multipliers; >
Большое количество вариантов приводит к тому, что двумя-тремя интеграционными тестами здесь уже не ограничишься. Минималистичный набор юнит-тестов отлично подойдет для отладки такого функционала.

Соответствующий набор тестов можно посмотреть в классе BonusPointCalculatorTest . Вот некоторые из них:
@Test public void calculate_oneProduct() < Product product = product("product").price("1.00").build(); Customer customer = customer("customer").build(); Mapquantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").build(); assertEquals(expectedBonus, bonus); > @Test public void calculate_favProduct() < Product product = product("product").price("1.00").build(); Customer customer = customer("customer").favProduct(product).build(); Mapquantities = mapOf(product, 1L); BigDecimal bonus = bonusPointCalculator.calculate(customer, list(product), quantities::get); BigDecimal expectedBonus = bonusPoints("0.10").addMultiplier(FAVORITE_MULTIPLIER).build(); assertEquals(expectedBonus, bonus); >
Стоит обратить внимание, что в тестах идет обращение именно к публичному API класса — методу calculate . Тестирование контракта класса, а не его реализации позволяет избегать поломок тестов из-за нефункциональных изменений и рефакторинга.
Наконец, когда мы проверили внутреннюю логику юнит-тестами, в интеграционный все эти детали выносить уже не нужно. В данном случае достаточно одного более-менее репрезентативного теста, например такого:
@Test public void calculateBonusPoints_twoProductTypes_correctValueCalculated() < Product product1 = product("product1").price("1.01").build(); Product product2 = product("product2").price("10.00").build(); productRepository.save(product1); productRepository.save(product2); Customer customer = customer("customer").build(); customerRepository.save(customer); Mapquantities = mapOf(product1.getId(), 1L, product2.getId(), 2L); BigDecimal bonus = customerController.calculateBonusPoints( new CalculateBonusPointsRequest("customer", quantities) ); BigDecimal bonusPointsProduct1 = bonusPoints("0.10").build(); BigDecimal bonusPointsProduct2 = bonusPoints("1.00").quantity(2).build(); BigDecimal expectedBonus = bonusPointsProduct1.add(bonusPointsProduct2); assertEquals(expectedBonus, bonus); >
Как и в случае с интеграционными тестами, использованный набор юнит-тестов очень небольшой и не гарантирует полной корректности приложения. Тем не менее его наличие значительно повышает уверенность в коде, облегчает отладку и дает прочие бонусы, перечисленные в первой части статьи.
Рекомендации по внедрению
Надеюсь, предыдущих разделов было достаточно, чтобы убедить хотя бы одного разработчика попробовать начать использовать тесты в своем проекте. В этой главе будут кратко перечислены основные рекомендации, которые помогут избежать серьезных проблем и приведут к снижению первичных издержек на внедрение.
Постарайтесь начать внедрение тестов на новом приложении. Написать первые тесты в большом legacy-проекте будет намного сложнее и потребует большей квалификации, чем в свежесозданном. Поэтому по возможности лучше начинать с небольшого нового приложения. Если же новых полноценных приложений не ожидается, можно попробовать разработать какую-нибудь полезную утилиту для внутреннего использования. Главное, чтобы задача была более-менее реалистичной — выдуманные примеры не дадут полноценного опыта.
Настройте регулярный запуск тестов. Если тесты не запускаются на регулярной основе, то они не только перестают выполнять свою основную функцию — проверку корректности кода, — но и быстро устаревают. Потому крайне важно настроить хотя бы минимальный CI-конвейер с автоматическим запуском тестов при каждом обновлении кода в репозитории.
Не гонитесь за покрытием. Как и в случае любой другой технологии, первое время тесты будут получаться не самого хорошего качества. Здесь может помочь соответствующая литература (ссылки в конце статьи) или грамотный ментор, но необходимости самостоятельного набивания шишек это не отменяет. Тесты в этом плане похожи на остальной код: понять, как они повлияют на проект, получится только пожив с ними некоторое время. Поэтому для минимизации ущерба первое время лучше не гнаться за количеством и красивыми цифрами вроде стопроцентного покрытия. Вместо этого стоит ограничиться основными позитивными сценариями по собственному функционалу приложения.
Не увлекайтесь юнит-тестами. В продолжение темы «количество vs качество» нужно отметить, что честными юнит-тестами первое время увлекаться не стоит, потому что это легко может привести к чрезмерной спецификации приложения. В свою очередь это станет серьезным тормозящим фактором при последующем рефакторинге и доработках приложения. Юнит-тесты следует использовать только при наличии сложной логики в конкретном классе или группе классов, которую неудобно проверять на уровне интеграционных.
Не увлекайтесь заглушками классов и методов приложения. Заглушки (stub, mock) — еще один инструмент, который требует взвешенного подхода и соблюдения баланса. С одной стороны, полная изоляция юнита позволяет сосредоточиться на тестируемой логике и не думать об остальных частях системы. С другой стороны, это потребует дополнительного времени на разработку и, как и при использовании юнит-тестов, может привести к чрезмерной спецификации поведения.
Отвяжите интеграционные тесты от внешних систем. Очень частая ошибка в интеграционных тестах — использование реальной базы данных, очередей сообщений и прочих внешних по отношению к приложению систем. Безусловно, возможность запустить тест в реальном окружении полезна для отладки и разработки. Такие тесты в небольших количествах могут иметь смысл, особенно для запуска в интерактивном режиме. Однако повсеместное их использование приводит к целому ряду проблем:
- Для запусков тестов нужно будет настраивать внешнее окружение. Например, устанавливать базу данных на каждую машину, где будет собираться приложение. Это усложнит вход новых разработчиков в проект и настройку CI.
- Состояние внешних систем может отличаться на разных машинах перед запуском тестов. Например, в базе могут уже находиться нужные приложению таблицы с данными, которые не ожидаются в тесте. Это приведет к непредсказуемым сбоям в работе тестов, и их устранение потребует значительного количества времени.
- В случае, если ведется параллельная работа над несколькими проектами, возможно неочевидное влияние одних проектов на другие. Например, специфические настройки базы, выполненные для одного из проектов, смогут помочь корректно работать функционалу другого проекта, который, однако, сломается при запуске на чистой базе на другой машине.
- Тесты выполняются долго: полный прогон может достигать десятков минут. Это приводит к тому, что разработчики перестают запускать тесты локально и смотрят на их результаты только после отправки изменений в удаленный репозиторий. Такое поведение сводит на нет большинство плюсов тестов, о которых говорилось в первой части статьи.
Следите за тем, чтобы тесты выполнялись за разумное время. Даже если тесты не зависят от реальных внешних систем, время их выполнения может легко выйти из-под контроля. Чтобы такого не происходило, нужно постоянно следить за этим показателем и принимать меры в случае необходимости. Самое меньшее, что можно сделать, — выделить медленные тесты в отдельную группу, чтобы они не мешали работе над не связанными с ними задачами.
Старайтесь делать тесты максимально понятными и читаемыми. Как уже было показано в примере, тесты надо писать так, чтобы в них не нужно было разбираться. Время, потраченное на изучение теста, могло бы быть потрачено на изучение кода.
Не зацикливайтесь на TDD (Test-Driven Development). TDD является довольно популярной практикой, однако я не считаю ее обязательной, особенно на первых этапах внедрения. В целом, умение писать хорошие тесты не связано с тем, в какой момент они написаны. Что действительно важно, так это делать первичную отладку кода уже на тестах, поскольку это один из основных способов экономии времени.
Первые тесты написаны, что дальше?
Далее надо внимательно наблюдать за жизнью тестов в проекте и периодически задавать себе вопросы, подобные следующим:
- Какие тесты мешают рефакторингу и доработкам (требуют постоянных исправлений)? Такие тесты требуется переписать либо полностью удалить из проекта и заменить более высокоуровневыми.
- Какие тесты часто и непредсказуемо ломаются при многократном либо параллельном запуске, при запуске в разных средах (компьютер коллеги, сервер CI)? Они также требуют переработки.
- Какие ошибки проходят мимо тестов? На каждый такой баг желательно добавлять новый тест и в будущем иметь их в виду при написании тестов для аналогичного функционала.
- Какие тесты работают слишком долго? Нужно постараться их переписать. Если это невозможно, то отделить их от более быстрых, чтобы сохранить возможность оперативного локального прогона.
Заключение
Поначалу лучше не гнаться за количеством тестов, а сосредоточиться на их качестве. Огромное число неуместных юнит-тестов может легко стать якорем, тянущим проект на дно. Кроме того, наличие юнит-тестов не освобождает от необходимости написания интеграционных. Поэтому наиболее эффективная стратегия на первое время — начинать с покрытия основных позитивных сценариев интеграционными тестами и, в случае если этого оказывается недостаточно, добавлять локальные проверки юнит-тестами. Со временем будет накапливаться обратная связь, которая поможет исправить допущенные ошибки и получить более четкое представление об эффективном использовании разных методик автоматического тестирования.
Надеюсь, среди прочитавших найдутся те, чьи тонкие струны души окажутся задеты моим графоманством, и в мире появится еще несколько проектов с хорошими и эффективными тестами!
Java Unit Testing: методики, понятия, практика


Что такое тест? Как гласит Вики: « Тест или испытание — способ изучения глубинных процессов деятельности системы посредством помещения системы в разные ситуации и отслеживание доступных наблюдению изменений в ней». Иными словами, это проверка правильности работы нашей системы в тех или иных ситуациях. Что же, посмотрим, какие вообще есть виды тестирования:
- Модульное тестирование (unit testing) — тесты, задача которых проверить каждый модуль системы по отдельности. Желательно, чтобы это были минимально делимые кусочки системы, например, модули.
- Системное тестирование (system testing) — тест высокого уровня для проверки работы большего куска приложения или системы в целом.
- Регрессионное тестирование (regression testing) — тестирование, которое используется для проверки того, не влияют ли новые фичи или исправленные баги на существующий функционал приложения и не появляются ли старые баги.
- Функциональное тестирование (functional testing) — проверка соответствия части приложения требованиям, заявленным в спецификациях, юзерсторях и т. д. Виды функционального тестирования:
- тест «белого ящика» (white box) на соответствие части приложения требованиям со знанием внутренней реализации системы;
- тест «черного ящика» (black box) на соответствие части приложения требованиям без знания внутренней реализации системы.
- Тестирование производительности (performance testing) — вид тестов, которые пишутся для определения скорости отработки системы или ее части под определённой нагрузкой.
- Нагрузочное тестирование (load testing) — тесты, предназначенные для проверки устойчивости системы при стандартных нагрузках и для нахождения максимально возможного пика, при котором приложение работает корректно.
- Стресс-тестирование (stress testing) — вид тестирования, предназначенный для проверки работоспособности приложения при нестандартных нагрузках и для определения максимально возможного пика, при котором система не упадёт.
- Тестирование безопасности (security testing) — тесты, используемые для проверки безопасности системы (от атак хакеров, вирусов, несанкционированного доступа к конфиденциальным данным и прочих радостей жизни).
- Тестирование локализации (localization testing) — это тесты локализации для приложения.
- Юзабилити тестирование (usability testing) — вид тестирования, направленный на проверку удобства использования, понятности, привлекательности и обучаемости для пользователей. Это всё звучит хорошо, но как оно происходит на практике? Все просто: используется пирамида тестирования Майка Кона: Это упрощенный вариант пирамиды: сейчас её делят на более мелкие детали. Но сегодня мы не будем извращаться и рассмотрим самый простой вариант.
- Unit — модульные тесты, применяемые в различных слоях приложения, тестирующие наименьшую делимую логику приложения: например, класс, но чаще всего — метод. Эти тесты обычно стараются по максимуму изолировать от внешней логики, то есть создать иллюзию того, что остальная часть приложения работает в стандартном режиме. Данных тестов всегда должно быть много (больше, чем остальных видов), так как они тестируют маленькие кусочки и весьма легковесные, не кушающие много ресурсов (под ресурсами я имею виду оперативную память и время).
- Integration — интеграционное тестирование. Оно проверяет более крупные кусочки системы, то есть это либо объединение нескольких кусочков логики (несколько методов или классов), либо корректность работы с внешним компонентом. Этих тестов как правило меньше, чем Unit, так как они тяжеловеснее. Как пример интеграционных тестов можно рассмотреть соединение с базой данных и проверку правильной отработки методов, работающих с ней.
- UI — тесты, которые проверяют работу пользовательского интерфейса. Они затрагивают логику на всех уровнях приложения, из-за чего их еще называют сквозными. Их как правило в разы меньше, так они наиболее тяжеловесны и должны проверять самые необходимые (используемые) пути. На рисунке выше мы видим соотношение площадей разных частей треугольника: примерно такая же пропорция сохраняется в количестве этих тестов в реальной работе. Сегодня подробно рассмотрим самые используемые тесты — юнит-тесты, так как уметь ими пользоваться на базовом уровне должны все уважающие себя Java-разработчики.
Ключевые понятия юнит-тестирования

Покрытие тестов (Code Coverage) — одна из главных оценок качества тестирования приложения. Это процент кода, который был покрыт тестами (0-100%). На практике многие гонятся за этим процентом, с чем я не согласен, так как начинается навешивание тестов там, где они не нужны. Например, у нас в сервисе есть стандартные CRUD (create/get/update/delete) операции без дополнительной логики. Эти методы — сугубо посредники, делегирующие работу слою, работающему с репозиторием. В данной ситуации нам нечего тестировать: разве то, что вызывает ли данный метод — метод из дао, но это не серьёзно. Для оценки покрытия тестами обычно используют дополнительные инструменты: JaCoCo, Cobertura, Clover, Emma и т.д. Для более детального изучения данного вопроса держи пару годных статей:
- материал о Code Coverage на JavaRush и на Хабре;
- фундаментальная теория тестирования.
TDD (Test-driven development) — разработка через тестирование. В рамках этого подхода в первую очередь пишется тест, который будет проверять определенный код. Получается тестирование чёрного ящика: мы знаем, что есть на входе и знаем, что должно получиться на выходе. Это позволяет избежать дублирования кода. Разработка через тестирование начинается с проектирования и разработки тестов для каждой небольшой функциональности приложения. В подходе TDD, во-первых, разрабатывается тест, который определяет и проверяет, что будет делать код. Основная цель TDD — сделать код более понятным, простым и без ошибок. Подход состоит из таких составляющих:
- Пишем наш тест.
- Запускаем тест, прошел он или нет (видим, что всё красное — не психуем: так и должно быть).
- Добавляем код, который должен удовлетворить данный тест (запускаем тест).
- Выполняем рефакторинг кода.
Исходя из того, что модульные тесты являются наименьшими элементами в пирамиде автоматизации тестирования, TDD основан на них. С помощью модульных тестов мы можем проверить бизнес-логику любого класса. BDD (Behavior-driven development) — разработка через поведение. Это подход основан на TDD. Если говорить точнее, он использует написанные понятным языком примеры (как правило на английском), которые иллюстрируют поведение системы для всех, кто участвует в разработке. Не будем углубляться в данный термин, так как он в основном затрагивает тестировщиков и бизнес-аналитиков. Тестовый сценарий (Test Case) — сценарий, описывающий шаги, конкретные условия и параметры, необходимые для проверки реализации тестируемого кода. Фикстуры (Fixture) — состояние среды тестирования, которое необходимо для успешного выполнения испытуемого метода. Это заранее заданный набор объектов и их поведения в используемых условиях.
Этапы тестирования

Тест состоит из трёх этапов:
- Задание тестируемых данных (фикстур).
- Использование тестируемого кода (вызов тестируемого метода).
- Проверка результатов и сверка с ожидаемыми.
Чтобы обеспечить модульность теста, нужно нужно изолироваться от других слоев приложения. Сделать это можно помощью заглушек, моков и шпионов. Мок (Mock) — объекты, которые настраиваются (например, специфично для каждого теста) и позволяют задать ожидания вызовы методов в виде ответов, которые мы планируем получить. Проверки соответствия ожиданиям проводятся через вызовы к Mock-объектам. Заглушки (Stub) — обеспечивают жестко зашитый ответ на вызовы во время тестирования. Также они могут сохранять в себе информацию о вызове (например, параметры или количество этих вызовов). Такие иногда называют своим термином — шпион ( Spy ). Иногда эти термины stubs и mock путают: разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок — это объект, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.
Среды тестирования

Итак, теперь ближе к делу. Для Java доступно несколько сред тестирования (фреймворков). Самые популярные из них — JUnit и TestNG. Для нашего обзора мы используем: JUnit тест представляет собой метод, содержащийся в классе, который используется только для тестирования. Класс, как правило, называется так же, как и класс, который он тестирует с +Test в конце. Например, CarService→ CarServiceTest. Система сборки Maven автоматически включает такие классы в тестовую область. По сути этот класс и называется тестовым. Немного пройдёмся по базовым аннотациям: @Test — определение данного метода в качестве тестируемого (по сути — метод, помеченный данной аннотацией и есть модульный тест). @Before — помечается метод, который будет выполняться перед каждым тестом. Например, заполнение тестовых данных класса, чтение входных данных и т. д. @After — ставится над методом, который будет вызывать после каждого теста (чистка данных, восстановление дефолтных значений). @BeforeClass — ставится над методом — аналог @Before. Но этот метод вызывается лишь однажды перед всеми тестами для данного класса и поэтому должен быть статическим. Он используется для выполнения более тяжелых операций, как например подъем тестовой БД. @AfterClass — противоположность @BeforeClass: исполняется один раз для данного класса, но исполняется после всех тестов. Используется, например, для очистки постоянных ресурсов или отключения от БД. @Ignore — отмечает, что метод ниже отключен и будет игнорироваться при общей прогонке тестов. Используется в разных случаях, например, если изменили базовый метод и не успели переделать под него тест. В таких случаях ещё желательно добавить описание — @Ignore(«Some description»). @Test (expected = Exception.class) — используется для отрицательных тестов. Это тесты, которые проверяют, как ведёт себя метод в случае ошибки, то есть тест ожидает, что метод выкинет некоторое исключение. Такой метод обозначается аннотацией @Test, но с указанием ошибки для отлова. @Test(timeout=100) — проверяет, что метод исполняется не более чем 100 миллисекунд. @Mock — используется над полем класс для задания данного объекта моком (это не из Junit библиотеки, а из Mockito), и если нам будет необходимо, мы зададим поведение мока в конкретной ситуации, непосредственно в методе теста. @RunWith(MockitoJUnitRunner.class) — метод ставится над классом. Он и является кнопкой для прогона тестов в нем. Runner-ы могут быть различными: например, есть такие: MockitoJUnitRunner, JUnitPlatform, SpringRunner и т. д.). В JUnit 5 аннотацию @RunWith заменили более мощной аннотацией @ExtendWith. Взглянем на некоторые методы сравнения результатов:
- assertEquals(Object expecteds, Object actuals) — проверяет, равны ли передаваемые обьекты.
- assertTrue(boolean flag) — проверяет, возвращает ли переданное значение — true.
- assertFalse(boolean flag) — проверяет, возвращает ли переданное значение — false.
- assertNull(Object object) – проверяет, является ли объект нулевым (null).
- assertSame(Object firstObject, Object secondObject) — проверяет, ссылаются ли передаваемые значения на один и тот же обьект.
- assertThat(T t, Matcher matcher) — проверяет, удовлетворяет ли t условию, указанному в matcher.
Ещё есть полезная форма сравнения из assertj — assertThat(firstObject).isEqualTo(secondObject) Здесь я рассказал о базовых методах, так как остальные — это различные вариации приведенных выше.
Практика тестирования
А теперь давайте рассмотрим приведенный выше материал на конкретном примере. Будем тестировать метод для сервиса — update. Рассматривать слой дао не будем, так как он у нас дефолтный. Добавим стартер для тестов:
org.springframework.boot spring-boot-starter-test 2.2.2.RELEASE test Итак, класс сервиса:
@Service @RequiredArgsConstructor public class RobotServiceImpl implements RobotService < private final RobotDAO robotDAO; @Override public Robot update(Long id, Robot robot) < Robot found = robotDAO.findById(id); return robotDAO.update(Robot.builder() .id(id) .name(robot.getName() != null ? robot.getName() : found.getName()) .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu()) .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer()) .build()); >>8 — вытягиваем обновляемый обьект из БД 9-14 — создаём объект через билдер, если в приходящем объекте есть поле — задаем его, если нет — оставляем то, что есть в БД И смотрим наш тест:
@RunWith(MockitoJUnitRunner.class) public class RobotServiceImplTest < @Mock private RobotDAO robotDAO; private RobotServiceImpl robotService; private static Robot testRobot; @BeforeClass public static void prepareTestData() < testRobot = Robot .builder() .id(123L) .name("testRobotMolly") .cpu("Intel Core i7-9700K") .producer("China") .build(); >@Before public void init()1 — наш Runner 4 — изолируем сервис от слоя дао, подставляя мок 11 — задаем для класса тестовую сущность (ту, которую мы будем юзать в качестве испытуемого хомячка) 22 — задаём объект сервиса, который мы и будем тестить
@Test public void updateTest()
Здесь мы видим четкое разделение теста на три части: 3-9 — задание фикстур 11 — выполнение тестируемой части 13-17 — проверка результатов Подробнее: 3-4 — задаём поведение для мока дао 5 — задаём экземпляр, который мы будем апдейтить поверх нашего стандартного 11 — используем метод и берём результирующий экземпляр 13 — проверяем, что он не ноль 14 — сверяем айди результата и заданные аргументы метода 15 — проверяем, обновилось ли имя 16 — смотрим результат по cpu 17 – так как в экземпляре для обновления мы не задавали это поле, оно должно остаться прежним, проверяем это.
Запускаем:
Тест зелёный, можно выдыхать)) Итак, подведём итоги: тестирование улучшает качество кода и делает процесс разработки более гибким и надёжный. Представьте себе, как много сил мы потратим при изменении дизайна программного обеспечения с сотнями файлов классов. Когда у нас есть модульные тесты, написанные для всех этих классов, мы можем уверенно провести рефакторинг. И самое главное — это помогает нам легко находить ошибки во время разработки. Гайз, на этом у меня сегодня всё: сыпем лайки, пишем комменты)))
JUnit
JUnit — это фреймворк для языка программирования Java, предназначенный для автоматического тестирования программ. Его основное назначение — unit-тестирование, то есть такое, когда по отдельности проверяется функциональность каждого компонента программы.


Освойте профессию «Java-разработчик»
Юнит-тестирование еще называют модульным. Благодаря ему программы работают как надо: возможные ошибки и непредвиденное поведение находят тестировщики, после чего программисты устраняют недочеты.
Автоматическое тестирование помогает сэкономить ресурсы: время специалистов, силы и средства. При этом оно точнее и быстрее, чем ручное. С помощью фреймворков, таких как JUnit, даже сложные тесты можно создавать легче и быстрее.
JUnit «вырос» из серии фреймворков xUnit — они есть для разных языков программирования и все ориентированы на тестирование. У них похожая архитектура и функциональность.
Кто пользуется JUnit
Фреймворк нужен тестировщикам-автоматизаторам, QA-инженерам — людям, которые занимаются проверкой функциональности программ. Знания JUnit пригодятся и для начинающих специалистов, чтобы эффективнее выполнять задачи, и для профи — чтобы грамотнее разрабатывать стратегии тестирования.
Иногда разработчики сами тестируют программы, которые пишут. В некоторых компаниях разработка построена так, что сначала код тестируют сами программисты, а потом он передается тестировщикам. В таких случаях разработчикам тоже бывают нужны соответствующие фреймворки, в том числе JUnit. Им пользуются специалисты, которые работают с языком Java.
Профессия / 14 месяцев
Java-разработчик
Освойте востребованный язык

Для чего используется JUnit
Это фреймворк для Java, полностью совместимый с самим языком и его инструментами, поэтому им удобнее всего пользоваться для тестирования Java-проектов. Это обычно продукты энтерпрайз-сектора: сервисы крупных компаний, банки, страховые и другие подобные направления.
Впрочем, версии фреймворка существуют и для других языков — например для PHP, Python, C# и прочих ЯП. Эти версии можно использовать и в проектах с иной технической базой. Поэтому JUnit актуален практически везде, где требуется быть знакомым с автоматическим юнит-тестированием. Чаще всего это проекты с принятой моделью TDD — Test-Driven Development, или разработка, построенная на тестах.
Что такое TDD
Test-Driven Development — подход к созданию программ, в котором на первом месте стоят тесты. На русский название можно перевести как «разработка через тестирование». Подход состоит из коротких циклов разработки, в которых сначала пишутся тесты, а уже потом — программы, проходящие их. То есть тесты выступают как своеобразное техзадание: в них прописывается, какие результаты должна выдавать программа в той или иной ситуации.
Код, который прошел заранее созданные тесты, после этого рефакторится, то есть переписывается под актуальные стандарты с сохранением логики и функциональности.
Создатели этого метода считали, что такой подход поможет вдохнуть в программистов уверенность и упростить проектирование программ. У него есть и еще одно достоинство — код тестируется сразу же, поэтому вероятность внезапной ошибки снижается. Но есть и риск: если в тестах пропустить какую-то важную деталь, она может всплыть позже как баг или сбой.

Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке
Как происходит тестирование с JUnit
Процесс автоматического тестирования такой: есть код, для него пишутся автотесты — небольшие программы, которые проверяют какую-либо возможность или компонент. Автотесты запускаются и работают с разными входными данными, чтобы протестировать работу основной программы в разных условиях.
Модульное тестирование, для которого используют JUnit, — это такой вид проверки, при котором каждый компонент рассматривается по отдельности. Оно помогает избежать ошибок на самом «низком» уровне — на уровне работоспособности отдельных объектов.
Поэтому процесс тестирования через JUnit можно представить так.
- Тестировщик знакомится с компонентом и с тем, как он должен работать. После этого он продумывает разные ситуации и решает, по какой логике будет работать тест.
- Тестировщик пишет код теста с помощью JUnit. Обычно этот код выполняет те действия, которые мог бы совершить с объектом человек или другой компонент программы. Там же сразу определяется, какие результаты тест должен воспринять как правильные.
- В тесте указываются разные входные данные, которые он будет «отдавать» компоненту в ходе тестирования.
- Тест настраивается: тестировщик определяет, что он будет выводить в случае ошибки, как покажет результаты, в каком порядке будет запускаться и так далее.
- После этого тестировщик запускает для компонента тест и проверяет результаты. Если нужно протестировать еще какую-то возможность, для этого пишется свой тест.
Как устроен тест на JUnit
Экземпляр тестовой программы создается как наследник от основного класса TestCase. У этого класса есть встроенные методы для инициализации, и их можно переопределить, если нужно, то есть переписать под свои нужды. После этого экземпляру прописываются методы — функции, конкретные тесты, каждый из которых выполняет свою задачу.
Код внутри метода — это какие-то действия, а за ними — проверки. То есть сначала тест делает что-то с компонентом, который проверяет, а потом «смотрит», какими получились результаты. Проверки показывают, соответствуют ли результаты тем, которые предполагались. Если это так — тест пройден. Если нет — в работе компонента есть ошибка и ее следует исправить.
Даже если только один из методов не прошел — или выдал исключение, весь тест считается проваленным.
Можно ли тестировать без JUnit
Да, это возможно. Есть ручное тестирование, есть другие фреймворки с иной функциональностью. Выбор конкретного инструмента зависит от команды разработчиков, целей и задач проекта, стека технологий и других факторов. JUnit чаще всего используют, когда проект пишут на Java, он довольно сложный и ручным тестированием не обойтись.
Маленькие проекты могут тестировать без мощных фреймворков: вручную или с помощью компактных библиотек. Но JUnit — инструмент, который серьезно упрощает этот процесс и позволяет выполнять сложные задачи. Поэтому, если тестируется что-то сложное, с большим количеством компонентов, с JUnit работать удобнее, чем без него.
Какие возможности дает JUnit
Так как JUnit — это фреймворк, у него более широкие возможности, чем у более узких и простых библиотек. Тест, написанный с его помощью, — это полноценная отдельная программа, пусть небольшая. У такого подхода есть свое достоинство: можно отделить тесты от основного кода, так что в финальной версии программы не будет ничего лишнего. Тестирующая программа в сборку не пойдет.
Юнит-тестирование с помощью JUnit — это еще и создание документации. Подробные тесты сами по себе говорят специалистам, что делает этот компонент и как он работает. Объяснять подобные вещи нужно: в коммерческой разработке часто встречаются ситуации, когда людям приходится работать с чужим кодом. Поэтому лучше заранее рассказать им все с помощью документации — так будет легче вникнуть.
Вот лишь несколько технических возможностей, которые дает JUnit. На самом деле их намного больше, но для полного описания понадобится целое руководство.
Собственная точка входа. Это то, о чем мы говорили выше: тест на JUnit — отдельная программа. Точка входа — это, например, метод main(), с которого начинается выполнение кода. Внутри этого метода находится вся программа.
Если у теста нет своей точки входа, значит, он выполняется в теле основной программы, а это не всегда удобно. А свой собственный метод для запуска позволяет ему существовать отдельно от продукта, и такое разделение делает код чище и понятнее.
Настройка тестов. Чтобы эффективно протестировать компонент, нужно предварительно настроить готовую программу-тест. То есть определить, когда и как она будет запускаться, инициализировать ее, чтобы привести в рабочее состояние, задать входные данные или сделать много чего еще. Для всего перечисленного в JUnit есть свои функции, которые позволяют выполнять эти действия буквально в две-три строчки кода.
Настройки в JUnit довольно гибкие. Можно отдельно настроить, какие действия будут выполняться перед всеми тестами, какие — перед каждым или перед некоторым конкретным. Есть и возможность описать действия после завершения теста — например, очистку памяти и удаление уже ненужных данных.

Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке
Совместное выполнение. JUnit позволяет параллельно запускать несколько тестов или объединять разные тестовые программы в набор. Это дает возможность использовать группу тестов как один, что помогает тестировщикам, например в ситуациях, когда разные тесты пишут разные люди.
Отключение тестов. Бывает так, что какой-то тест сейчас запускать не нужно. Остальные должны отработать, а один конкретный — нет. JUnit позволяет отключать такие тесты с помощью специальной команды @Ignore. Ее можно поставить на отдельный метод тестовой программы или на нее целиком.
Тайм-аут. По умолчанию у тестов нет временных ограничений. Но в реальности, если компонент работает слишком долго, это неправильно и мешает использовать программу. И скорость работы тоже можно протестировать с помощью JUnit – в нем есть встроенное правило, которое позволяет задать тайм-аут. Это значит, что, если тест не выполнится за заданное время, он считается проваленным. Такой подход помогает отслеживать, например, зависания или неэффективные решения, из-за которых код начинает работать очень медленно.
Динамические тесты. Начиная с версии JUnit 5 во фреймворке появилась возможность создавать и запускать динамические тесты. Их основное отличие в том, что такие тесты выполняются для программы не в момент компиляции, а в момент запуска. Это расширяет возможности тестирования. Например, появляется возможность запускать тест в цикле с разными параметрами, причем так, чтобы это воспринималось как один тест, а не несколько разных.
Преимущества JUnit
«Чистый» Java. Тестировщику или разработчику, скорее всего, не понадобится работать с какими-то другими языками и надстройками. Фреймворк реализован на чистом Java, полностью поддерживает его принципы программирования, согласования по части именования, синтаксис и другие особенности. Поэтому тем, кто уже знаком с Java, освоить его будет легко.
Совместимость с Java-инструментами. По этой же причине JUnit полностью совместим с инструментами, которыми обычно пользуются разработчики на Java. Например, он прекрасно работает с Maven и Gradle — это программное обеспечение, которое отвечает за сборку Java-проектов.
Есть у него и обратная совместимость: это значит, что старые программы могут работать с новыми версиями JUnit. Например, если какая-то программа писалась, когда был актуален JUnit 4, то она поймет и JUnit 5, а он в свою очередь поймет ее.
Ориентированность на TDD. У разработки через тестирование есть ряд достоинств, и она довольно популярна. JUnit позволяет полностью реализовать ее принципы, поэтому инструмент не придется серьезно переделывать под этот подход. Внедрить TDD становится легче, а работать с ней — удобнее.
Популярность. JUnit — очень распространенный инструмент для модульного тестирования, поэтому по нему всегда много материалов для тестировщиков разных уровней. По этой причине с ним довольно легко работать: сообщество широкое, всегда можно задать интересующий вопрос или проконсультироваться с другими специалистами. Энтузиасты могут писать для фреймворка свои инструменты или давать советы по его использованию, а у многих задач уже есть типичные решения. Это помогает и в обучении, и в работе.
Недостатки JUnit
JUnit сильно ориентирован на Java: в нем те же особенности именования и довольно многословный код. Это плюс для Java-разработчиков и минус для многих других специалистов. Человеку, который не разбирается в Java, будет непросто разобраться в тексте кода. А если просмотреть тест понадобится специалисту из совершенно другой отрасли, он, вероятно, ничего не поймет.
Еще один минус — отсутствие встроенных mock-объектов. Так называются сущности-заглушки: они «имитируют» функции настоящих объектов, которые будут работать в коде. У них более примитивное устройство по сравнению с реальными объектами и жестко заданный функционал. Ими часто пользуются в тестировании, но в JUnit нет для них механизма: приходится подключать дополнительную библиотеку Mockito.
Другие недостатки скорее субъективны. Например, часть разработчиков не любит JUnit из-за того, что он пересоздает экземпляр тестовой программы для каждого выполнения тестового метода. Но это не объективный минус, а скорее особенность, которая не всем нравится.
Что понадобится для начала работы с JUnit
Фреймворк можно загрузить из официального репозитория на GitHub, а можно подключить с помощью утилит для разработчиков. Чтобы писать тесты, также понадобятся установленный язык Java, среда разработки для него и одна из программ для сборки — чаще всего это Maven. В качестве среды обычно используют IntelliJ IDEA, некоторые также пользуются Eclipse.
Кроме перечисленного, понадобится компонент, который вы собираетесь тестировать, и умение работать с Java. Код для автоматических тестов пишется на этом языке: если вы с ним незнакомы, понадобится изучить хотя бы его основы.
Как начать изучать JUnit
Начните с «чистого» Java. Когда вы освоитесь в особенностях этого языка и научитесь писать на нем простой код, можете переходить к написанию тестов. Для тестирования важно не столько понимать тонкости языка программирования, сколько иметь соответствующий образ мышления. Важно быть внимательным и скрупулезным, уметь замечать мелочи и продумывать множество возможных исходов. Все это — навыки, которые нарабатываются со временем.
Получить техническую базу и научиться писать тесты на JUnit вы можете на наших профессиональных курсах. Записывайтесь, мы поможем получить новую профессию.
Java-разработчик
Java уже 20 лет в мировом топе языков программирования. На нем создают сложные финансовые сервисы, стриминги и маркетплейсы. Освойте технологии, которые нужны для backend-разработки, за 14 месяцев.

Статьи по теме:
- Maven
- Какие стереотипы мешают начать карьеру в IT
Как писать простейшие UnitTest’ы к простейшим функциям?
Хочу научиться писать тесты для своих проектов. Подскажите какие-нибудь хорошие ресурсы, чтобы научиться тестировать Android приложения. Насколько важно их использование? Сейчас я пишу приложение без их использования и пока не могу оценить их пользу. Те коды, которые встречаю в интернете, не помню, чтобы где-то в коде встречал тесты. В общем, хочу понять, что это значит. Подскажите, с чего начать. Допустим есть вот такой метод:
private File getFile(File path)
Как можно к нему написать тест и что нужно для этого сделать? ПРАВКА Вот кстати есть ссылка с видео где показан пример теста https://www.youtube.com/watch?v=ZJE0MDKJOow
Отслеживать
33.9k 25 25 золотых знаков 130 130 серебряных знаков 222 222 бронзовых знака
задан 12 июн 2016 в 17:21
10.9k 18 18 золотых знаков 62 62 серебряных знака 128 128 бронзовых знаков
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
Для того чтобы создать юнит тест, вам прежде всего нужно определиться что вы собственно собираетесь тестировать. В идеале, ваш метод должен делать что-то одно и тогда ваша задача упрощается. Если возможно, то юнит тест должен тестировать метод как черный ящик, то есть, вы подаете что-то на вход и проверяете полученное значение. К сожалению это не всегда возможно.
В вашем конкретном случае, первое что бросается в глаза, это то что метод помечен как private , такой метод нельзя тестировать стандартными методами. Существует много теорий насчет того нужно ли тестировать приватные методы или нет. Мое мнение — их можно не тестировать. Правильность работы приватных методов будет проверена неявно когда вы тестируете все открытые методы. Хотя если вы очень хотите то можно пометить метод как ‘default’ (убрать тип доступa), или можно использовать reflection.
Но допустим ваш метод публичный, тогда анализируя код понимаем, что метод возвращает новый файл с именем построенным из пути и функции текущей даты. То есть, он делает два действия: * Создает имя файла * Создает собственно файл Тестировать его в текущем виде можно но не интересно. Я бы посоветовал немного поправить ваш код чтобы он был более пригодным для тестирования:
public File getFileFullName(File path, Date date) < String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); String fullPath = path.getPath() + File.separator + timeStamp + ".html"; return fullPath; >// где-то в вашем коде String path = . Date now = new Date(); String fileFullName = getFileFullName(path, now); File file = new File(fileFullName); // тест для метода getFileFullName в классе ClassUnderTest @Test public void testGetFileFullName() < String path = "/abc"; Date date = new Date(1465953124); //2016-06-15 13:12:06 ClassUnderTest instance = new ClassUnderTest() String name = instance.getFileFullName(path, date); assertEquals("/abc/20160615_131206.html"); >
Но вы можете спросить, а как тестировать код где мы создаем дату или используем файл? Касательно даты, я бы рекомендовал вообще не использовать new Date() в коде. Вместо этого использовать что типа провайдера или сервиса для получения текущей даты/времени. Тогда вы можете подменять реализацию этого класса для тестирования. Простейшая реализация может быть синглтон:
public class DateTimeUtils < private static DateTime fixedTime; public static DateTime getCurrentDateTime() < if (fixedTime == null) < return new DateTime(DateTimeZone.UTC); >return fixedTime; > public static void useFixedCurrentTime(DateTime timeToReturn) < fixedTime = timeToReturn; >>
Что касается объекта File то здесь вам поможет подмена классов с помощью мокирования. Посмотрите Mockito или PowerMock.
Еще маленькое дополнение. Если бы вы использовали test-driven development то такой проблемы бы не было.
